From b0b6e037ac0bdfb5ef8b91f70459e310940503f4 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 3 Jun 2015 16:57:17 -0400
Subject: [PATCH 01/39] CC-6046, CC-6045, CC-6047 - New SoundCloud
 implementation

---
 airtime_mvc/application/Bootstrap.php         |    1 +
 airtime_mvc/application/configs/ACL.php       |    4 +-
 .../application/configs/airtime-conf.php      |    2 +-
 .../configs/classmap-airtime-conf.php         |    7 +
 airtime_mvc/application/configs/conf.php      |   12 +-
 .../application/controllers/ApiController.php |    5 -
 .../controllers/ErrorController.php           |    1 +
 .../controllers/LibraryController.php         |   48 +-
 .../controllers/PreferenceController.php      |   11 +-
 .../controllers/ScheduleController.php        |   19 -
 .../controllers/SoundcloudController.php      |   78 ++
 .../controllers/ThirdPartyController.php      |   55 +
 .../controllers/UpgradeController.php         |   43 +-
 .../upgrade_sql/airtime_2.5.13/upgrade.sql    |   14 +
 .../forms/EmailServerPreferences.php          |    1 -
 airtime_mvc/application/forms/Preferences.php |    2 +-
 .../forms/SoundCloudPreferences.php           |   81 ++
 .../forms/SoundcloudPreferences.php           |  142 ---
 .../customvalidators/PasswordNotEmpty.php     |   18 -
 airtime_mvc/application/models/Preference.php |  132 +-
 .../application/models/ShowBuilder.php        |   10 -
 .../application/models/ShowInstance.php       |   13 -
 airtime_mvc/application/models/Soundcloud.php |   99 --
 airtime_mvc/application/models/StoredFile.php |  133 +-
 .../airtime/ThirdPartyTrackReferences.php     |   18 +
 .../airtime/ThirdPartyTrackReferencesPeer.php |   18 +
 .../ThirdPartyTrackReferencesQuery.php        |   18 +
 .../map/CcPlayoutHistoryTemplateTableMap.php  |    1 +
 .../airtime/map/CcShowInstancesTableMap.php   |    2 +-
 .../models/airtime/map/CcShowTableMap.php     |    2 +-
 .../map/ThirdPartyTrackReferencesTableMap.php |   58 +
 .../om/BaseCcPlayoutHistoryTemplate.php       |  285 +++++
 .../om/BaseCcPlayoutHistoryTemplatePeer.php   |    3 +
 .../om/BaseCcPlayoutHistoryTemplateQuery.php  |   78 ++
 .../om/BaseThirdPartyTrackReferences.php      | 1107 +++++++++++++++++
 .../om/BaseThirdPartyTrackReferencesPeer.php  | 1014 +++++++++++++++
 .../om/BaseThirdPartyTrackReferencesQuery.php |  516 ++++++++
 .../application/services/CalendarService.php  |   17 -
 .../services/SoundCloudService.php            |  164 +++
 .../services/ThirdPartyService.php            |  120 ++
 airtime_mvc/application/upgrade/Upgrades.php  |   84 +-
 .../views/scripts/error/error-404.phtml       |   18 +
 .../views/scripts/form/preferences.phtml      |    6 +
 .../scripts/form/preferences_soundcloud.phtml |  134 +-
 airtime_mvc/build/build.properties            |    2 +-
 airtime_mvc/build/schema.xml                  |   10 +
 airtime_mvc/build/sql/schema.sql              |   21 +
 airtime_mvc/library/soundcloud-api/README.md  |  114 --
 .../soundcloud-api/Services/Soundcloud.php    |  737 -----------
 .../Services/Soundcloud/Exception.php         |  146 ---
 .../Services/Soundcloud/Version.php           |   22 -
 .../soundcloud-api/tests/Soundcloud_Test.php  |  310 -----
 .../tests/Soundcloud_Test_Helper.php          |   94 --
 .../public/js/airtime/library/library.js      |  143 +--
 .../js/airtime/preferences/preferences.js     |   47 +-
 .../schedule/full-calendar-functions.js       |   46 +-
 .../public/js/airtime/schedule/schedule.js    |   64 +-
 .../public/js/airtime/showbuilder/builder.js  |    2 +-
 composer.json                                 |    3 +-
 composer.lock                                 |   50 +-
 utils/soundcloud-uploader                     |   34 -
 utils/soundcloud-uploader.php                 |   61 -
 62 files changed, 4009 insertions(+), 2491 deletions(-)
 create mode 100644 airtime_mvc/application/controllers/SoundcloudController.php
 create mode 100644 airtime_mvc/application/controllers/ThirdPartyController.php
 create mode 100644 airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
 create mode 100644 airtime_mvc/application/forms/SoundCloudPreferences.php
 delete mode 100644 airtime_mvc/application/forms/SoundcloudPreferences.php
 delete mode 100644 airtime_mvc/application/forms/customvalidators/PasswordNotEmpty.php
 delete mode 100644 airtime_mvc/application/models/Soundcloud.php
 create mode 100644 airtime_mvc/application/models/airtime/ThirdPartyTrackReferences.php
 create mode 100644 airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesPeer.php
 create mode 100644 airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesQuery.php
 create mode 100644 airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
 create mode 100644 airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
 create mode 100644 airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
 create mode 100644 airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
 create mode 100644 airtime_mvc/application/services/SoundCloudService.php
 create mode 100644 airtime_mvc/application/services/ThirdPartyService.php
 create mode 100644 airtime_mvc/application/views/scripts/error/error-404.phtml
 delete mode 100644 airtime_mvc/library/soundcloud-api/README.md
 delete mode 100644 airtime_mvc/library/soundcloud-api/Services/Soundcloud.php
 delete mode 100644 airtime_mvc/library/soundcloud-api/Services/Soundcloud/Exception.php
 delete mode 100644 airtime_mvc/library/soundcloud-api/Services/Soundcloud/Version.php
 delete mode 100644 airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test.php
 delete mode 100644 airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test_Helper.php
 delete mode 100755 utils/soundcloud-uploader
 delete mode 100644 utils/soundcloud-uploader.php

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index f7af625b3..33ad21d84 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -27,6 +27,7 @@ require_once "ProvisioningHelper.php";
 require_once "GoogleAnalytics.php";
 require_once "Timezone.php";
 require_once "Auth.php";
+require_once __DIR__ . '/services/SoundCloudService.php';
 require_once __DIR__.'/forms/helpers/ValidationTypes.php';
 require_once __DIR__.'/forms/helpers/CustomDecorators.php';
 require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php';
diff --git a/airtime_mvc/application/configs/ACL.php b/airtime_mvc/application/configs/ACL.php
index c58986f1e..cde876b35 100644
--- a/airtime_mvc/application/configs/ACL.php
+++ b/airtime_mvc/application/configs/ACL.php
@@ -38,7 +38,8 @@ $ccAcl->add(new Zend_Acl_Resource('library'))
       ->add(new Zend_Acl_Resource('billing'))
       ->add(new Zend_Acl_Resource('thank-you'))
       ->add(new Zend_Acl_Resource('provisioning'))
-      ->add(new Zend_Acl_Resource('player'));
+      ->add(new Zend_Acl_Resource('player'))
+      ->add(new Zend_Acl_Resource('soundcloud'));
 
 /** Creating permissions */
 $ccAcl->allow('G', 'index')
@@ -72,6 +73,7 @@ $ccAcl->allow('G', 'index')
       ->allow('A', 'systemstatus')
       ->allow('A', 'preference')
       ->allow('A', 'player')
+      ->allow('A', 'soundcloud')
       ->allow('S', 'thank-you')
       ->allow('S', 'billing');
       
diff --git a/airtime_mvc/application/configs/airtime-conf.php b/airtime_mvc/application/configs/airtime-conf.php
index b23a37334..ad939b2ce 100644
--- a/airtime_mvc/application/configs/airtime-conf.php
+++ b/airtime_mvc/application/configs/airtime-conf.php
@@ -1,6 +1,6 @@
 <?php
 // This file generated by Propel 1.7.0 convert-conf target
-// from XML runtime conf file /home/ubuntu/airtime/airtime_mvc/build/runtime-conf.xml
+// from XML runtime conf file /home/sourcefabric/dev/Airtime/airtime_mvc/build/runtime-conf.xml
 $conf = array (
   'datasources' => 
   array (
diff --git a/airtime_mvc/application/configs/classmap-airtime-conf.php b/airtime_mvc/application/configs/classmap-airtime-conf.php
index 814429f76..d6f5159e2 100644
--- a/airtime_mvc/application/configs/classmap-airtime-conf.php
+++ b/airtime_mvc/application/configs/classmap-airtime-conf.php
@@ -103,6 +103,9 @@ return array (
   'BaseCloudFile' => 'airtime/om/BaseCloudFile.php',
   'BaseCloudFilePeer' => 'airtime/om/BaseCloudFilePeer.php',
   'BaseCloudFileQuery' => 'airtime/om/BaseCloudFileQuery.php',
+  'BaseThirdPartyTrackReferences' => 'airtime/om/BaseThirdPartyTrackReferences.php',
+  'BaseThirdPartyTrackReferencesPeer' => 'airtime/om/BaseThirdPartyTrackReferencesPeer.php',
+  'BaseThirdPartyTrackReferencesQuery' => 'airtime/om/BaseThirdPartyTrackReferencesQuery.php',
   'CcBlock' => 'airtime/CcBlock.php',
   'CcBlockPeer' => 'airtime/CcBlockPeer.php',
   'CcBlockQuery' => 'airtime/CcBlockQuery.php',
@@ -239,4 +242,8 @@ return array (
   'CloudFilePeer' => 'airtime/CloudFilePeer.php',
   'CloudFileQuery' => 'airtime/CloudFileQuery.php',
   'CloudFileTableMap' => 'airtime/map/CloudFileTableMap.php',
+  'ThirdPartyTrackReferences' => 'airtime/ThirdPartyTrackReferences.php',
+  'ThirdPartyTrackReferencesPeer' => 'airtime/ThirdPartyTrackReferencesPeer.php',
+  'ThirdPartyTrackReferencesQuery' => 'airtime/ThirdPartyTrackReferencesQuery.php',
+  'ThirdPartyTrackReferencesTableMap' => 'airtime/map/ThirdPartyTrackReferencesTableMap.php',
 );
\ No newline at end of file
diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php
index f89a5472e..27c1a0071 100644
--- a/airtime_mvc/application/configs/conf.php
+++ b/airtime_mvc/application/configs/conf.php
@@ -89,7 +89,17 @@ class Config {
 
         $CC_CONFIG['soundcloud-connection-retries'] = $values['soundcloud']['connection_retries'];
         $CC_CONFIG['soundcloud-connection-wait'] = $values['soundcloud']['time_between_retries'];
-        
+
+        $globalAirtimeConfig = "/etc/airtime-saas/".$CC_CONFIG['dev_env']."/airtime.conf";
+        if (!file_exists($globalAirtimeConfig)) {
+            // If the dev env specific airtime.conf doesn't exist default
+            // to the production airtime.conf
+            $globalAirtimeConfig = "/etc/airtime-saas/production/airtime.conf";
+        }
+        $globalAirtimeConfigValues = parse_ini_file($globalAirtimeConfig, true);
+        $CC_CONFIG['soundcloud-client-id'] = $globalAirtimeConfigValues['soundcloud']['soundcloud_client_id'];
+        $CC_CONFIG['soundcloud-client-secret'] = $globalAirtimeConfigValues['soundcloud']['soundcloud_client_secret'];
+
         if(isset($values['demo']['demo'])){
             $CC_CONFIG['demo'] = $values['demo']['demo'];
         }
diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php
index 490800085..97d7ccb4d 100644
--- a/airtime_mvc/application/controllers/ApiController.php
+++ b/airtime_mvc/application/controllers/ApiController.php
@@ -717,11 +717,6 @@ class ApiController extends Zend_Controller_Action
         // fields
         $file->setMetadataValue('MDATA_KEY_CREATOR', "Airtime Show Recorder");
         $file->setMetadataValue('MDATA_KEY_TRACKNUMBER', $show_instance_id);
-
-        if (!$showCanceled && Application_Model_Preference::GetAutoUploadRecordedShowToSoundcloud()) {
-            $id = $file->getId();
-            Application_Model_Soundcloud::uploadSoundcloud($id);
-        }
     }
 
     public function mediaMonitorSetupAction()
diff --git a/airtime_mvc/application/controllers/ErrorController.php b/airtime_mvc/application/controllers/ErrorController.php
index 8a62d9ea6..315da3ac9 100644
--- a/airtime_mvc/application/controllers/ErrorController.php
+++ b/airtime_mvc/application/controllers/ErrorController.php
@@ -71,6 +71,7 @@ class ErrorController extends Zend_Controller_Action {
      * 404 error - route or controller
      */
     public function error404Action() {
+        Logging::info("404!");
         $this->_helper->viewRenderer('error-404');
         $this->getResponse()->setHttpResponseCode(404);
         $this->view->message = _('Page not found.');
diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index 212d57521..07bbc109b 100644
--- a/airtime_mvc/application/controllers/LibraryController.php
+++ b/airtime_mvc/application/controllers/LibraryController.php
@@ -265,8 +265,9 @@ class LibraryController extends Zend_Controller_Action
             }
         }
 
-        //SOUNDCLOUD MENU OPTIONS
-        if ($type === "audioclip" && Application_Model_Preference::GetUploadToSoundcloudOption()) {
+        // SOUNDCLOUD MENU OPTION
+        $soundcloudService = new SoundcloudService();
+        if ($type === "audioclip" && $soundcloudService->hasAccessToken()) {
 
             //create a menu separator
             $menu["sep1"] = "-----------";
@@ -274,20 +275,16 @@ class LibraryController extends Zend_Controller_Action
             //create a sub menu for Soundcloud actions.
             $menu["soundcloud"] = array("name" => _("Soundcloud"), "icon" => "soundcloud", "items" => array());
 
-            $scid = $file->getSoundCloudId();
-
-            if ($scid > 0) {
-                $url = $file->getSoundCloudLinkToFile();
-                $menu["soundcloud"]["items"]["view"] = array("name" => _("View on Soundcloud"), "icon" => "soundcloud", "url" => $url);
-            }
-
-            if (!is_null($scid)) {
+            $serviceId = $soundcloudService->getServiceId($id);
+            if (!is_null($file) && $serviceId != 0) {
+                $menu["soundcloud"]["items"]["view"] = array("name" => _("View on Soundcloud"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/view-on-sound-cloud/id/{$id}");
                 $text = _("Re-upload to SoundCloud");
             } else {
                 $text = _("Upload to SoundCloud");
             }
 
-            $menu["soundcloud"]["items"]["upload"] = array("name" => $text, "icon" => "soundcloud", "url" => $baseUrl."library/upload-file-soundcloud/id/{$id}");
+            // TODO: reimplement how this works
+            $menu["soundcloud"]["items"]["upload"] = array("name" => $text, "icon" => "soundcloud", "url" => $baseUrl."soundcloud/upload/id/{$id}");
         }
 
         if (empty($menu)) {
@@ -525,33 +522,4 @@ class LibraryController extends Zend_Controller_Action
             Logging::info($e->getMessage());
         }
     }
-
-    public function uploadFileSoundcloudAction()
-    {
-        $id = $this->_getParam('id');
-        Application_Model_Soundcloud::uploadSoundcloud($id);
-        // we should die with ui info
-        $this->_helper->json->sendJson(null);
-    }
-
-    public function getUploadToSoundcloudStatusAction()
-    {
-        $id = $this->_getParam('id');
-        $type = $this->_getParam('type');
-
-        if ($type == "show") {
-            $show_instance = new Application_Model_ShowInstance($id);
-            $this->view->sc_id = $show_instance->getSoundCloudFileId();
-            $file = $show_instance->getRecordedFile();
-            $this->view->error_code = $file->getSoundCloudErrorCode();
-            $this->view->error_msg = $file->getSoundCloudErrorMsg();
-        } elseif ($type == "file") {
-            $file                   = Application_Model_StoredFile::RecallById($id);
-            $this->view->sc_id      = $file->getSoundCloudId();
-            $this->view->error_code = $file->getSoundCloudErrorCode();
-            $this->view->error_msg  = $file->getSoundCloudErrorMsg();
-        } else {
-            Logging::warn("Trying to upload unknown type: $type with id: $id");
-        }
-    }
 }
diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php
index 3a0df1505..401e58af9 100644
--- a/airtime_mvc/application/controllers/PreferenceController.php
+++ b/airtime_mvc/application/controllers/PreferenceController.php
@@ -62,14 +62,9 @@ class PreferenceController extends Zend_Controller_Action
                 Application_Model_Preference::setTuneinPartnerKey($values["tunein_partner_key"]);
                 Application_Model_Preference::setTuneinPartnerId($values["tunein_partner_id"]);
 
-                /*Application_Model_Preference::SetUploadToSoundcloudOption($values["UploadToSoundcloudOption"]);
-                Application_Model_Preference::SetSoundCloudDownloadbleOption($values["SoundCloudDownloadbleOption"]);
-                Application_Model_Preference::SetSoundCloudUser($values["SoundCloudUser"]);
-                Application_Model_Preference::SetSoundCloudPassword($values["SoundCloudPassword"]);
-                Application_Model_Preference::SetSoundCloudTags($values["SoundCloudTags"]);
-                Application_Model_Preference::SetSoundCloudGenre($values["SoundCloudGenre"]);
-                Application_Model_Preference::SetSoundCloudTrackType($values["SoundCloudTrackType"]);
-                Application_Model_Preference::SetSoundCloudLicense($values["SoundCloudLicense"]);*/
+                // SoundCloud Preferences
+                Application_Model_Preference::setDefaultSoundCloudLicenseType($values["SoundCloudLicense"]);
+                Application_Model_Preference::setDefaultSoundCloudSharingType($values["SoundCloudSharing"]);
 
                 $this->view->statusMsg = "<div class='success'>". _("Preferences updated.")."</div>";
                 $this->view->form = $form;
diff --git a/airtime_mvc/application/controllers/ScheduleController.php b/airtime_mvc/application/controllers/ScheduleController.php
index 1890ac200..4760e7f3d 100644
--- a/airtime_mvc/application/controllers/ScheduleController.php
+++ b/airtime_mvc/application/controllers/ScheduleController.php
@@ -253,25 +253,6 @@ class ScheduleController extends Zend_Controller_Action
         $this->view->show_id = $showId;
     }
 
-    public function uploadToSoundCloudAction()
-    {
-        $show_instance = $this->_getParam('id');
-        
-        try {
-            $show_inst = new Application_Model_ShowInstance($show_instance);
-        } catch (Exception $e) {
-            $this->view->show_error = true;
-
-            return false;
-        }
-
-        $file = $show_inst->getRecordedFile();
-        $id = $file->getId();
-        Application_Model_Soundcloud::uploadSoundcloud($id);
-        // we should die with ui info
-        $this->_helper->json->sendJson(null);
-    }
-
     public function makeContextMenuAction()
     {
         $instanceId = $this->_getParam('instanceId');
diff --git a/airtime_mvc/application/controllers/SoundcloudController.php b/airtime_mvc/application/controllers/SoundcloudController.php
new file mode 100644
index 000000000..b265f5740
--- /dev/null
+++ b/airtime_mvc/application/controllers/SoundcloudController.php
@@ -0,0 +1,78 @@
+<?php
+
+require_once "ThirdPartyController.php";
+require_once "ise/php-soundcloud/src/Soundcloud/Service.php";
+
+class SoundcloudController extends ThirdPartyController {
+
+    /**
+     * @var SoundcloudService
+     */
+    private $_soundcloudService;
+
+    /**
+     * Set up SoundCloud access variables.
+     */
+    public function init() {
+        parent::init();
+        $this->_soundcloudService = new SoundcloudService();
+    }
+
+    /**
+     * Send user to SoundCloud to authorize before being redirected
+     */
+    public function authorizeAction() {
+        $auth_url = $this->_soundcloudService->getAuthorizeUrl();
+        header('Location: ' . $auth_url);
+    }
+
+    /**
+     * Called when user successfully completes SoundCloud authorization.
+     * Store the returned request token for future requests.
+     */
+    public function redirectAction() {
+        $code = $_GET['code'];
+        $this->_soundcloudService->requestNewAccessToken($code);
+        header('Location: ' . $this->_baseUrl . 'Preference'); // Redirect back to the Preference page
+    }
+
+    /**
+     * Fetch the permalink to a file on SoundCloud and redirect to it.
+     */
+    public function viewOnSoundCloudAction() {
+        $request = $this->getRequest();
+        $id = $request->getParam('id');
+        try {
+            $soundcloudLink = $this->_soundcloudService->getLinkToFile($id);
+            header('Location: ' . $soundcloudLink);
+        } catch (Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
+            // If we end up here it means the track was removed from SoundCloud
+            // or the foreign id in our database is incorrect, so we should just
+            // get rid of the database record
+            Logging::warn("Error retrieving track data from SoundCloud: " . $e->getMessage());
+            $this->_soundcloudService->removeTrackReference($id);
+            // Redirect to a 404 so the user knows something went wrong
+            header('Location: ' . $this->_baseUrl . 'error/error-404'); // Redirect back to the Preference page
+        }
+    }
+
+    /**
+     * Upload the file with the given id to SoundCloud.
+     *
+     * @throws Zend_Controller_Response_Exception thrown if upload fails for any reason
+     */
+    public function uploadAction() {
+        $request = $this->getRequest();
+        $id = $request->getParam('id');
+        $this->_soundcloudService->upload($id);
+    }
+
+    /**
+     * Clear the previously saved request token from the preferences.
+     */
+    public function deauthorizeAction() {
+        Application_Model_Preference::setSoundCloudRequestToken("");
+        header('Location: ' . $this->_baseUrl . 'Preference'); // Redirect back to the Preference page
+    }
+
+}
diff --git a/airtime_mvc/application/controllers/ThirdPartyController.php b/airtime_mvc/application/controllers/ThirdPartyController.php
new file mode 100644
index 000000000..2fec86161
--- /dev/null
+++ b/airtime_mvc/application/controllers/ThirdPartyController.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Class ThirdPartyController abstract superclass for third-party service authorization
+ */
+abstract class ThirdPartyController extends Zend_Controller_Action {
+
+    /**
+     * @var string base url and port for redirection
+     */
+    protected $_baseUrl;
+
+    /**
+     * Disable controller rendering and initialize
+     */
+    public function init() {
+        $CC_CONFIG = Config::getConfig();
+        $this->_baseUrl = 'http://' . $CC_CONFIG['baseUrl'] . ":" . $CC_CONFIG['basePort'] . "/";
+
+        $this->view->layout()->disableLayout(); // Don't inject the standard Now Playing header.
+        $this->_helper->viewRenderer->setNoRender(true); // Don't use (phtml) templates
+    }
+
+    /**
+     * Send user to a third-party service to authorize before being redirected
+     *
+     * @return void
+     */
+    abstract function authorizeAction();
+
+    /**
+     * Called when user successfully completes third-party authorization.
+     * Store the returned request token for future requests.
+     *
+     * @return void
+     */
+    abstract function redirectAction();
+
+    /**
+     * Upload the file with the given id to a third-party service.
+     *
+     * @return void
+     *
+     * @throws Zend_Controller_Response_Exception thrown if upload fails for any reason
+     */
+    abstract function uploadAction();
+
+    /**
+     * Clear the previously saved request token from the preferences.
+     *
+     * @return void
+     */
+    abstract function deauthorizeAction();
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php
index 518e173ac..d3cee649b 100644
--- a/airtime_mvc/application/controllers/UpgradeController.php
+++ b/airtime_mvc/application/controllers/UpgradeController.php
@@ -9,25 +9,21 @@ class UpgradeController extends Zend_Controller_Action
         $this->view->layout()->disableLayout();
         $this->_helper->viewRenderer->setNoRender(true);
         
-        if (!$this->verifyAuth()) {
+        if (!RestAuth::verifyAuth(true, false, $this)) {
             return;
         }
 
-        $upgraders = array();
-        array_push($upgraders, new AirtimeUpgrader253());        
-        array_push($upgraders, new AirtimeUpgrader254());
-        array_push($upgraders, new AirtimeUpgrader255());
-        array_push($upgraders, new AirtimeUpgrader259());
-        array_push($upgraders, new AirtimeUpgrader2510());
-        array_push($upgraders, new AirtimeUpgrader2511());
-        array_push($upgraders, new AirtimeUpgrader2512());
+        // Get all upgrades dynamically so we don't have to add them explicitly each time
+        $upgraders = getUpgrades();
+        Logging::info($upgraders);
 
         $didWePerformAnUpgrade = false;
         try 
         {
-            for ($i = 0; $i < count($upgraders); $i++)
+            foreach ($upgraders as $upgrader)
             {
-                $upgrader = $upgraders[$i];
+                /** @var $upgrader AirtimeUpgrader */
+                $upgrader = new $upgrader();
                 if ($upgrader->checkIfUpgradeSupported())
                 {
                 	// pass __DIR__ to the upgrades, since __DIR__ returns parent dir of file, not executor
@@ -36,7 +32,6 @@ class UpgradeController extends Zend_Controller_Action
                     $this->getResponse()
                          ->setHttpResponseCode(200)
                          ->appendBody("Upgrade to Airtime " . $upgrader->getNewVersion() . " OK<br>"); 
-                    $i = 0; //Start over, in case the upgrade handlers are not in ascending order.
                 }
             }
             
@@ -54,28 +49,4 @@ class UpgradeController extends Zend_Controller_Action
            		 ->appendBody($e->getMessage());
         }
     }
-
-    private function verifyAuth()
-    {
-        //The API key is passed in via HTTP "basic authentication":
-        //http://en.wikipedia.org/wiki/Basic_access_authentication
-        
-        $CC_CONFIG = Config::getConfig();
-        
-        //Decode the API key that was passed to us in the HTTP request.
-        $authHeader = $this->getRequest()->getHeader("Authorization");
-
-        $encodedRequestApiKey = substr($authHeader, strlen("Basic "));
-        $encodedStoredApiKey = base64_encode($CC_CONFIG["apiKey"][0] . ":");
-
-        if ($encodedRequestApiKey !== $encodedStoredApiKey)
-        {
-            $this->getResponse()
-                 ->setHttpResponseCode(401)
-                 ->appendBody("Error: Incorrect API key.<br>");
-            return false;
-        }
-        return true;
-    }
-
 }
diff --git a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
new file mode 100644
index 000000000..e2d051bff
--- /dev/null
+++ b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
@@ -0,0 +1,14 @@
+CREATE TABLE IF NOT EXISTS "third_party_track_references"
+(
+    "id" serial NOT NULL,
+    "service" VARCHAR(512) NOT NULL,
+    "foreign_id" INTEGER NOT NULL,
+    "file_id" INTEGER NOT NULL,
+    "status" VARCHAR(256) NOT NULL,
+    PRIMARY KEY ("id")
+);
+
+ALTER TABLE "third_party_track_references" ADD CONSTRAINT "track_reference_fkey"
+    FOREIGN KEY ("file_id")
+    REFERENCES "cc_files" ("id")
+    ON DELETE CASCADE;
diff --git a/airtime_mvc/application/forms/EmailServerPreferences.php b/airtime_mvc/application/forms/EmailServerPreferences.php
index d49cdd3fb..1e0feda79 100644
--- a/airtime_mvc/application/forms/EmailServerPreferences.php
+++ b/airtime_mvc/application/forms/EmailServerPreferences.php
@@ -1,6 +1,5 @@
 <?php
 require_once 'customvalidators/ConditionalNotEmpty.php';
-require_once 'customvalidators/PasswordNotEmpty.php';
 
 class Application_Form_EmailServerPreferences extends Zend_Form_SubForm
 {
diff --git a/airtime_mvc/application/forms/Preferences.php b/airtime_mvc/application/forms/Preferences.php
index d410ed3c6..cb201cd2b 100644
--- a/airtime_mvc/application/forms/Preferences.php
+++ b/airtime_mvc/application/forms/Preferences.php
@@ -30,7 +30,7 @@ class Application_Form_Preferences extends Zend_Form
         $tuneinPreferences = new Application_Form_TuneInPreferences();
         $this->addSubForm($tuneinPreferences, 'preferences_tunein');
 
-        $soundcloud_pref = new Application_Form_SoundcloudPreferences();
+        $soundcloud_pref = new Application_Form_SoundCloudPreferences();
         $this->addSubForm($soundcloud_pref, 'preferences_soundcloud');
 
         $danger_pref = new Application_Form_DangerousPreferences();
diff --git a/airtime_mvc/application/forms/SoundCloudPreferences.php b/airtime_mvc/application/forms/SoundCloudPreferences.php
new file mode 100644
index 000000000..fbf744520
--- /dev/null
+++ b/airtime_mvc/application/forms/SoundCloudPreferences.php
@@ -0,0 +1,81 @@
+<?php
+require_once 'customvalidators/ConditionalNotEmpty.php';
+
+class Application_Form_SoundcloudPreferences extends Zend_Form_SubForm
+{
+
+    public function init()
+    {
+        $this->setDecorators(array(
+            array('ViewScript', array('viewScript' => 'form/preferences_soundcloud.phtml'))
+        ));
+
+//        $select = new Zend_Form_Element_Select('SoundCloudTrackType');
+//        $select->setLabel(_('Default Track Type:'));
+//        $select->setAttrib('class', 'input_select');
+//        $select->setMultiOptions(array(
+//                "" => "",
+//                "original" => _("Original"),
+//                "remix" => _("Remix"),
+//                "live" => _("Live"),
+//                "recording" => _("Recording"),
+//                "spoken" => _("Spoken"),
+//                "podcast" => _("Podcast"),
+//                "demo" => _("Demo"),
+//                "in progress" => _("Work in progress"),
+//                "stem" => _("Stem"),
+//                "loop" => _("Loop"),
+//                "sound effect" => _("Sound Effect"),
+//                "sample" => _("One Shot Sample"),
+//                "other" => _("Other")
+//            ));
+//        $select->setRequired(false);
+//        $select->setValue(Application_Model_Preference::GetSoundCloudTrackType());
+//        $select->setDecorators(array('ViewHelper'));
+//        $this->addElement($select);
+
+        $select = new Zend_Form_Element_Select('SoundCloudLicense');
+        $select->setLabel(_('Default License:'));
+        $select->setAttrib('class', 'input_select');
+        $select->setMultiOptions(array(
+                                     "no-rights-reserved" => _("The work is in the public domain"),
+                                     "all-rights-reserved" => _("All rights are reserved"),
+                                     "cc-by" => _("Creative Commons Attribution"),
+                                     "cc-by-nc" => _("Creative Commons Attribution Noncommercial"),
+                                     "cc-by-nd" => _("Creative Commons Attribution No Derivative Works"),
+                                     "cc-by-sa" => _("Creative Commons Attribution Share Alike"),
+                                     "cc-by-nc-nd" => _("Creative Commons Attribution Noncommercial Non Derivate Works"),
+                                     "cc-by-nc-sa" => _("Creative Commons Attribution Noncommercial Share Alike")
+                                 ));
+        $select->setRequired(false);
+        $select->setValue(Application_Model_Preference::getDefaultSoundCloudLicenseType());
+        $this->addElement($select);
+
+        $select = new Zend_Form_Element_Select('SoundCloudSharing');
+        $select->setLabel(_('Default Sharing Type:'));
+        $select->setAttrib('class', 'input_select');
+        $select->setMultiOptions(array(
+                                     "public" => _("Public"),
+                                     "private" => _("Private"),
+                                 ));
+        $select->setRequired(false);
+        $select->setValue(Application_Model_Preference::getDefaultSoundCloudSharingType());
+        $this->addElement($select);
+
+        $this->addElement('image', 'SoundCloudConnect', array(
+            'src'        => 'http://connect.soundcloud.com/2/btn-connect-sc-l.png',
+            'decorators' => array(
+                'ViewHelper'
+            )
+        ));
+
+        $this->addElement('image', 'SoundCloudDisconnect', array(
+            'src'        => 'http://connect.soundcloud.com/2/btn-disconnect-l.png',
+            'decorators' => array(
+                'ViewHelper'
+            )
+        ));
+
+    }
+
+}
diff --git a/airtime_mvc/application/forms/SoundcloudPreferences.php b/airtime_mvc/application/forms/SoundcloudPreferences.php
deleted file mode 100644
index 3d1563a65..000000000
--- a/airtime_mvc/application/forms/SoundcloudPreferences.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-require_once 'customvalidators/ConditionalNotEmpty.php';
-require_once 'customvalidators/PasswordNotEmpty.php';
-
-class Application_Form_SoundcloudPreferences extends Zend_Form_SubForm
-{
-
-    public function init()
-    {
-        $this->setDecorators(array(
-            array('ViewScript', array('viewScript' => 'form/preferences_soundcloud.phtml'))
-        ));
-
-        //enable soundcloud uploads option
-        $this->addElement('checkbox', 'UploadToSoundcloudOption', array(
-            'label'      => _('Enable SoundCloud Upload'),
-            'required'   => false,
-            'value' => Application_Model_Preference::GetUploadToSoundcloudOption(),
-            'decorators' => array(
-                'ViewHelper'
-            )
-        ));
-
-        //enable downloadable for soundcloud
-        $this->addElement('checkbox', 'SoundCloudDownloadbleOption', array(
-            'label'      => _('Automatically Mark Files "Downloadable" on SoundCloud'),
-            'required'   => false,
-            'value' => Application_Model_Preference::GetSoundCloudDownloadbleOption(),
-            'decorators' => array(
-                'ViewHelper'
-            )
-        ));
-
-        //SoundCloud Username
-        $this->addElement('text', 'SoundCloudUser', array(
-            'class'      => 'input_text',
-            'label'      => _('SoundCloud Email'),
-            'filters'    => array('StringTrim'),
-            'autocomplete' => 'off',
-            'value' => Application_Model_Preference::GetSoundCloudUser(),
-            'decorators' => array(
-                'ViewHelper'
-            ),
-
-            // By default, 'allowEmpty' is true. This means that our custom
-            // validators are going to be skipped if this field is empty,
-            // which is something we don't want
-            'allowEmpty' => false,
-            'validators' => array(
-                new ConditionalNotEmpty(array('UploadToSoundcloudOption'=>'1'))
-            )
-        ));
-
-        //SoundCloud Password
-        $this->addElement('password', 'SoundCloudPassword', array(
-            'class'      => 'input_text',
-            'label'      => _('SoundCloud Password'),
-            'filters'    => array('StringTrim'),
-            'autocomplete' => 'off',
-            'value' => Application_Model_Preference::GetSoundCloudPassword(),
-            'decorators' => array(
-                'ViewHelper'
-            ),
-
-            // By default, 'allowEmpty' is true. This means that our custom
-            // validators are going to be skipped if this field is empty,
-            // which is something we don't want
-            'allowEmpty' => false,
-            'validators' => array(
-                new ConditionalNotEmpty(array('UploadToSoundcloudOption'=>'1'))
-            ),
-            'renderPassword' => true
-        ));
-
-         // Add the description element
-        $this->addElement('textarea', 'SoundCloudTags', array(
-            'label'      => _('SoundCloud Tags: (separate tags with spaces)'),
-            'required'   => false,
-            'class'      => 'input_text_area',
-            'value' => Application_Model_Preference::GetSoundCloudTags(),
-            'decorators' => array(
-                'ViewHelper'
-            )
-        ));
-
-        //SoundCloud default genre
-        $this->addElement('text', 'SoundCloudGenre', array(
-            'class'      => 'input_text',
-            'label'      => _('Default Genre:'),
-            'required'   => false,
-            'filters'    => array('StringTrim'),
-            'value' => Application_Model_Preference::GetSoundCloudGenre(),
-            'decorators' => array(
-                'ViewHelper'
-            )
-        ));
-
-        $select = new Zend_Form_Element_Select('SoundCloudTrackType');
-        $select->setLabel(_('Default Track Type:'));
-        $select->setAttrib('class', 'input_select');
-        $select->setMultiOptions(array(
-                "" => "",
-                "original" => _("Original"),
-                "remix" => _("Remix"),
-                "live" => _("Live"),
-                "recording" => _("Recording"),
-                "spoken" => _("Spoken"),
-                "podcast" => _("Podcast"),
-                "demo" => _("Demo"),
-                "in progress" => _("Work in progress"),
-                "stem" => _("Stem"),
-                "loop" => _("Loop"),
-                "sound effect" => _("Sound Effect"),
-                "sample" => _("One Shot Sample"),
-                "other" => _("Other")
-            ));
-        $select->setRequired(false);
-        $select->setValue(Application_Model_Preference::GetSoundCloudTrackType());
-        $select->setDecorators(array('ViewHelper'));
-        $this->addElement($select);
-
-        $select = new Zend_Form_Element_Select('SoundCloudLicense');
-        $select->setLabel(_('Default License:'));
-        $select->setAttrib('class', 'input_select');
-        $select->setMultiOptions(array(
-                "" => "",
-                "no-rights-reserved" => _("The work is in the public domain"),
-                "all-rights-reserved" => _("All rights are reserved"),
-                "cc-by" => _("Creative Commons Attribution"),
-                "cc-by-nc" => _("Creative Commons Attribution Noncommercial"),
-                "cc-by-nd" => _("Creative Commons Attribution No Derivative Works"),
-                "cc-by-sa" => _("Creative Commons Attribution Share Alike"),
-                "cc-by-nc-nd" => _("Creative Commons Attribution Noncommercial Non Derivate Works"),
-                "cc-by-nc-sa" => _("Creative Commons Attribution Noncommercial Share Alike")
-            ));
-        $select->setRequired(false);
-        $select->setValue(Application_Model_Preference::GetSoundCloudLicense());
-        $select->setDecorators(array('ViewHelper'));
-        $this->addElement($select);
-    }
-
-}
diff --git a/airtime_mvc/application/forms/customvalidators/PasswordNotEmpty.php b/airtime_mvc/application/forms/customvalidators/PasswordNotEmpty.php
deleted file mode 100644
index 5073c5d80..000000000
--- a/airtime_mvc/application/forms/customvalidators/PasswordNotEmpty.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-class PasswordNotEmpty extends ConditionalNotEmpty
-{
-    public function isValid($value, $context = null)
-    {
-        $result = parent::isValid($value, $context);
-        if (!$result) {
-            // allow empty if username/email was set before and didn't change
-            $storedUser = Application_Model_Preference::GetSoundCloudUser();
-            if ($storedUser != '' && $storedUser == $context['SoundCloudUser']) {
-                return true;
-            }
-        }
-
-        return $result;
-    }
-}
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index 541870fde..7fa917ba8 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -329,77 +329,6 @@ class Application_Model_Preference
         self::setValue("station_name", $station_name);
     }
 
-    public static function SetAutoUploadRecordedShowToSoundcloud($upload)
-    {
-        self::setValue("soundcloud_auto_upload_recorded_show", $upload);
-    }
-
-    public static function GetAutoUploadRecordedShowToSoundcloud()
-    {
-        return self::getValue("soundcloud_auto_upload_recorded_show");
-    }
-
-    public static function SetSoundCloudUser($user)
-    {
-        self::setValue("soundcloud_user", $user);
-    }
-
-    public static function GetSoundCloudUser()
-    {
-        return self::getValue("soundcloud_user");
-    }
-
-    public static function SetSoundCloudPassword($password)
-    {
-        if (strlen($password) > 0)
-            self::setValue("soundcloud_password", $password);
-    }
-
-    public static function GetSoundCloudPassword()
-    {
-        return self::getValue("soundcloud_password");
-    }
-
-    public static function SetSoundCloudTags($tags)
-    {
-        self::setValue("soundcloud_tags", $tags);
-    }
-
-    public static function GetSoundCloudTags()
-    {
-        return self::getValue("soundcloud_tags");
-    }
-
-    public static function SetSoundCloudGenre($genre)
-    {
-        self::setValue("soundcloud_genre", $genre);
-    }
-
-    public static function GetSoundCloudGenre()
-    {
-        return self::getValue("soundcloud_genre");
-    }
-
-    public static function SetSoundCloudTrackType($track_type)
-    {
-        self::setValue("soundcloud_tracktype", $track_type);
-    }
-
-    public static function GetSoundCloudTrackType()
-    {
-        return self::getValue("soundcloud_tracktype");
-    }
-
-    public static function SetSoundCloudLicense($license)
-    {
-        self::setValue("soundcloud_license", $license);
-    }
-
-    public static function GetSoundCloudLicense()
-    {
-        return self::getValue("soundcloud_license");
-    }
-
     public static function SetAllow3rdPartyApi($bool)
     {
         self::setValue("third_party_api", $bool);
@@ -660,12 +589,6 @@ class Application_Model_Preference
 
         $outputArray['LIVE_DURATION'] = Application_Model_LiveLog::GetLiveShowDuration($p_testing);
         $outputArray['SCHEDULED_DURATION'] = Application_Model_LiveLog::GetScheduledDuration($p_testing);
-        $outputArray['SOUNDCLOUD_ENABLED'] = self::GetUploadToSoundcloudOption();
-        if ($outputArray['SOUNDCLOUD_ENABLED']) {
-            $outputArray['NUM_SOUNDCLOUD_TRACKS_UPLOADED'] = Application_Model_StoredFile::getSoundCloudUploads();
-        } else {
-            $outputArray['NUM_SOUNDCLOUD_TRACKS_UPLOADED'] = NULL;
-        }
 
         $outputArray['STATION_NAME'] = self::GetStationName();
         $outputArray['PHONE'] = self::GetPhone();
@@ -711,12 +634,6 @@ class Application_Model_Preference
                         $outputString .= "\t".strtoupper($k)." : ".$v."\n";
                     }
                 }
-            } elseif ($key == "SOUNDCLOUD_ENABLED") {
-                if ($out) {
-                    $outputString .= $key." : TRUE\n";
-                } elseif (!$out) {
-                    $outputString .= $key." : FALSE\n";
-                }
             } elseif ($key == "SAAS") {
                 $outputString .= $key.' : '.$out."\n";
             } else {
@@ -930,26 +847,6 @@ class Application_Model_Preference
         }
     }
 
-    public static function SetUploadToSoundcloudOption($upload)
-    {
-        self::setValue("soundcloud_upload_option", $upload);
-    }
-
-    public static function GetUploadToSoundcloudOption()
-    {
-        return self::getValue("soundcloud_upload_option");
-    }
-
-    public static function SetSoundCloudDownloadbleOption($upload)
-    {
-        self::setValue("soundcloud_downloadable", $upload);
-    }
-
-    public static function GetSoundCloudDownloadbleOption()
-    {
-        return self::getValue("soundcloud_downloadable");
-    }
-
     public static function SetWeekStartDay($day)
     {
         self::setValue("week_start_day", $day);
@@ -1503,4 +1400,33 @@ class Application_Model_Preference
     {
         self::setValue("last_tunein_metadata_update", $value);
     }
+
+    /* Third Party */
+
+    // SoundCloud
+
+    public static function getDefaultSoundCloudLicenseType() {
+        return self::getValue("soundcloud_license_type");
+    }
+
+    public static function setDefaultSoundCloudLicenseType($value) {
+        self::setValue("soundcloud_license_type", $value);
+    }
+
+    public static function getDefaultSoundCloudSharingType() {
+        return self::getValue("soundcloud_sharing_type");
+    }
+
+    public static function setDefaultSoundCloudSharingType($value) {
+        self::setValue("soundcloud_sharing_type", $value);
+    }
+
+    public static function getSoundCloudRequestToken() {
+        return self::getValue("soundcloud_request_token");
+    }
+
+    public static function setSoundCloudRequestToken($value) {
+        self::setValue("soundcloud_request_token", $value);
+    }
+
 }
diff --git a/airtime_mvc/application/models/ShowBuilder.php b/airtime_mvc/application/models/ShowBuilder.php
index 2352bedb4..bd57fdff8 100644
--- a/airtime_mvc/application/models/ShowBuilder.php
+++ b/airtime_mvc/application/models/ShowBuilder.php
@@ -212,16 +212,6 @@ class Application_Model_ShowBuilder
             $row["rebroadcast_title"] = sprintf(_("Rebroadcast of %s from %s"), $name, $time);
         } elseif (intval($p_item["si_record"]) === 1) {
             $row["record"] = true;
-
-            // at the time of creating on show, the recorded file is not in the DB yet.
-            // therefore, 'si_file_id' is null. So we need to check it.
-            if (Application_Model_Preference::GetUploadToSoundcloudOption() && isset($p_item['si_file_id'])) {
-                $file = Application_Model_StoredFile::RecallById($p_item['si_file_id']);
-                if (isset($file)) {
-                    $sid = $file->getSoundCloudId();
-                    $row['soundcloud_id'] = $sid;
-                }
-            }
         }
 
         if ($startsEpoch < $this->epoch_now && $endsEpoch > $this->epoch_now) {
diff --git a/airtime_mvc/application/models/ShowInstance.php b/airtime_mvc/application/models/ShowInstance.php
index e931b93b9..55d559ce5 100644
--- a/airtime_mvc/application/models/ShowInstance.php
+++ b/airtime_mvc/application/models/ShowInstance.php
@@ -118,19 +118,6 @@ SQL;
         return $showStartExplode[1];
     }
 
-    public function setSoundCloudFileId($p_soundcloud_id)
-    {
-        $file = Application_Model_StoredFile::RecallById($this->_showInstance->getDbRecordedFile());
-        $file->setSoundCloudFileId($p_soundcloud_id);
-    }
-
-    public function getSoundCloudFileId()
-    {
-        $file = Application_Model_StoredFile::RecallById($this->_showInstance->getDbRecordedFile());
-
-        return $file->getSoundCloudId();
-    }
-
     public function getRecordedFile()
     {
         $file_id =  $this->_showInstance->getDbRecordedFile();
diff --git a/airtime_mvc/application/models/Soundcloud.php b/airtime_mvc/application/models/Soundcloud.php
deleted file mode 100644
index 6c7b181d7..000000000
--- a/airtime_mvc/application/models/Soundcloud.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-require_once 'soundcloud-api/Services/Soundcloud.php';
-
-class Application_Model_Soundcloud
-{
-    private $_soundcloud;
-
-    public function __construct()
-    {
-        $CC_CONFIG = Config::getConfig();
-        $this->_soundcloud = new Services_Soundcloud(
-            $CC_CONFIG['soundcloud-client-id'],
-            $CC_CONFIG['soundcloud-client-secret']);
-    }
-
-    private function getToken()
-    {
-        $username = Application_Model_Preference::GetSoundCloudUser();
-        $password = Application_Model_Preference::GetSoundCloudPassword();
-
-        $token = $this->_soundcloud->accessTokenResourceOwner($username, $password);
-
-        return $token;
-    }
-
-    public function uploadTrack($filepath, $filename, $description,
-        $tags=array(), $release=null, $genre=null)
-    {
-
-        if (!$this->getToken()) {
-            throw new NoSoundCloundToken();
-        }
-        if (count($tags)) {
-            $tags = join(" ", $tags);
-            $tags = $tags." ".Application_Model_Preference::GetSoundCloudTags();
-        } else {
-            $tags = Application_Model_Preference::GetSoundCloudTags();
-        }
-
-        $downloadable = Application_Model_Preference::GetSoundCloudDownloadbleOption() == '1';
-
-        $track_data = array(
-            'track[sharing]'      => 'private',
-            'track[title]'        => $filename,
-            'track[asset_data]'   => '@' . $filepath,
-            'track[tag_list]'     => $tags,
-            'track[description]'  => $description,
-            'track[downloadable]' => $downloadable,
-
-        );
-
-        if (isset($release)) {
-            $release = str_replace(" ", "-", $release);
-            $release = str_replace(":", "-", $release);
-
-            //YYYY-MM-DD-HH-mm-SS
-            $release = explode("-", $release);
-            $track_data['track[release_year]']  = $release[0];
-            $track_data['track[release_month]'] = $release[1];
-            $track_data['track[release_day]']   = $release[2];
-        }
-
-        if (isset($genre) && $genre != "") {
-            $track_data['track[genre]'] = $genre;
-        } else {
-            $default_genre = Application_Model_Preference::GetSoundCloudGenre();
-            if ($default_genre != "") {
-                $track_data['track[genre]'] = $default_genre;
-            }
-        }
-
-        $track_type = Application_Model_Preference::GetSoundCloudTrackType();
-        if ($track_type != "") {
-            $track_data['track[track_type]'] = $track_type;
-        }
-
-        $license = Application_Model_Preference::GetSoundCloudLicense();
-        if ($license != "") {
-            $track_data['track[license]'] = $license;
-        }
-
-        $response = json_decode(
-            $this->_soundcloud->post('tracks', $track_data),
-            true
-        );
-
-        return $response;
-
-    }
-
-    public static function uploadSoundcloud($id)
-    {
-        $cmd = "/usr/lib/airtime/utils/soundcloud-uploader $id > /dev/null &";
-        Logging::info("Uploading soundcloud with command: $cmd");
-        exec($cmd);
-    }
-}
-
-class NoSoundCloundToken extends Exception {}
diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php
index 32d6ec371..8c5eb809f 100644
--- a/airtime_mvc/application/models/StoredFile.php
+++ b/airtime_mvc/application/models/StoredFile.php
@@ -616,6 +616,13 @@ SQL;
 
     /* TODO: Callers of this function should use a Propel transaction. Start
      * by creating $con outside the function with beingTransaction() */
+    /**
+     * @param int $p_id
+     * @param \Doctrine\DBAL\Driver\PDOConnection $con
+     *
+     * @return Application_Model_StoredFile
+     * @throws Exception
+     */
     public static function RecallById($p_id=null, $con=null) {
         //TODO
         if (is_null($con)) {
@@ -898,10 +905,6 @@ SQL;
                 $formatter = new BitrateFormatter($row['bit_rate']);
                 $row['bit_rate'] = $formatter->format();
 
-                //soundcloud status
-                $file = Application_Model_StoredFile::RecallById($row['id']);
-                $row['soundcloud_id'] = $file->getSoundCloudId();
-
                 // for audio preview
                 $row['audioFile'] = $row['id'].".".pathinfo($row['filepath'], PATHINFO_EXTENSION);
 
@@ -1131,77 +1134,6 @@ SQL;
         return $rows;
     }
 
-    /* Gets number of tracks uploaded to
-     * Soundcloud in the last 24 hours
-     */
-    public static function getSoundCloudUploads()
-    {
-        try {
-
-            $sql = <<<SQL
-SELECT soundcloud_id AS id,
-       soundcloud_upload_time
-FROM CC_FILES
-WHERE (id != -2
-       AND id != -3)
-  AND (soundcloud_upload_time >= (now() - (INTERVAL '1 day')))
-SQL;
-
-            $rows = Application_Common_Database::prepareAndExecute($sql);
-
-            return count($rows);
-        } catch (Exception $e) {
-            header('HTTP/1.0 503 Service Unavailable');
-            Logging::info("Could not connect to database.");
-            exit;
-        }
-
-    }
-
-    public function setSoundCloudLinkToFile($link_to_file)
-    {
-        $this->_file->setDbSoundCloudLinkToFile($link_to_file)
-        ->save();
-    }
-
-    public function getSoundCloudLinkToFile()
-    {
-        return $this->_file->getDbSoundCloudLinkToFile();
-    }
-
-    public function setSoundCloudFileId($p_soundcloud_id)
-    {
-        $this->_file->setDbSoundCloudId($p_soundcloud_id)
-            ->save();
-    }
-
-    public function getSoundCloudId()
-    {
-        return $this->_file->getDbSoundCloudId();
-    }
-
-    public function setSoundCloudErrorCode($code)
-    {
-        $this->_file->setDbSoundCloudErrorCode($code)
-            ->save();
-    }
-
-    public function getSoundCloudErrorCode()
-    {
-        return $this->_file->getDbSoundCloudErrorCode();
-    }
-
-    public function setSoundCloudErrorMsg($msg)
-    {
-        $this->_file->setDbSoundCloudErrorMsg($msg)
-            ->save();
-    }
-
-    public function getSoundCloudErrorMsg()
-    {
-        return $this->_file->getDbSoundCloudErrorMsg();
-    }
-
     public function getDirectory()
     {
         return $this->_file->getDbDirectory();
@@ -1217,12 +1149,6 @@ SQL;
         $this->_file->setDbHidden($flag)
             ->save();
     }
-    public function setSoundCloudUploadTime($time)
-    {
-        $this->_file->setDbSoundCloundUploadTime($time)
-            ->save();
-    }
-
 
     // This method seems to be unsued everywhere so I've commented it out
     // If it's absence does not have any effect then it will be completely
@@ -1237,51 +1163,6 @@ SQL;
         return $this->_file->getDbOwnerId();
     }
 
-    // note: never call this method from controllers because it does a sleep
-    public function uploadToSoundCloud()
-    {
-        $CC_CONFIG = Config::getConfig();
-
-        $file = $this->_file;
-        if (is_null($file)) {
-            return "File does not exist";
-        }
-        if (Application_Model_Preference::GetUploadToSoundcloudOption()) {
-            for ($i=0; $i<$CC_CONFIG['soundcloud-connection-retries']; $i++) {
-                $description = $file->getDbTrackTitle();
-                $tag         = array();
-                $genre       = $file->getDbGenre();
-                $release     = $file->getDbUtime();
-                try {
-                    $filePaths = $this->getFilePaths();
-                    $filePath = $filePaths[0];
-                    $soundcloud     = new Application_Model_Soundcloud();
-                    $soundcloud_res = $soundcloud->uploadTrack(
-                        $filePath, $this->getName(), $description,
-                        $tag, $release, $genre);
-                    $this->setSoundCloudFileId($soundcloud_res['id']);
-                    $this->setSoundCloudLinkToFile($soundcloud_res['permalink_url']);
-                    $this->setSoundCloudUploadTime(new DateTime("now"), new DateTimeZone("UTC"));
-                    break;
-                } catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-                    $code = $e->getHttpCode();
-                    $msg  = $e->getHttpBody();
-                    // TODO : Do not parse JSON by hand
-                    $temp = explode('"error":',$msg);
-                    $msg  = trim($temp[1], '"}');
-                    $this->setSoundCloudErrorCode($code);
-                    $this->setSoundCloudErrorMsg($msg);
-                    // setting sc id to -3 which indicates error
-                    $this->setSoundCloudFileId(SOUNDCLOUD_ERROR);
-                    if (!in_array($code, array(0, 100))) {
-                        break;
-                    }
-                }
-
-                sleep($CC_CONFIG['soundcloud-connection-wait']);
-            }
-        }
-    }
 
     public static function setIsPlaylist($p_playlistItems, $p_type, $p_status) {
         foreach ($p_playlistItems as $item) {
diff --git a/airtime_mvc/application/models/airtime/ThirdPartyTrackReferences.php b/airtime_mvc/application/models/airtime/ThirdPartyTrackReferences.php
new file mode 100644
index 000000000..6cd1cc7a4
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/ThirdPartyTrackReferences.php
@@ -0,0 +1,18 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for representing a row from the 'third_party_track_references' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.airtime
+ */
+class ThirdPartyTrackReferences extends BaseThirdPartyTrackReferences
+{
+}
diff --git a/airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesPeer.php b/airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesPeer.php
new file mode 100644
index 000000000..d3842ca49
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesPeer.php
@@ -0,0 +1,18 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for performing query and update operations on the 'third_party_track_references' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.airtime
+ */
+class ThirdPartyTrackReferencesPeer extends BaseThirdPartyTrackReferencesPeer
+{
+}
diff --git a/airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesQuery.php b/airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesQuery.php
new file mode 100644
index 000000000..10a2dde79
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/ThirdPartyTrackReferencesQuery.php
@@ -0,0 +1,18 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for performing query and update operations on the 'third_party_track_references' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.airtime
+ */
+class ThirdPartyTrackReferencesQuery extends BaseThirdPartyTrackReferencesQuery
+{
+}
diff --git a/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php b/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php
index 78be57d28..23c00c076 100644
--- a/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php
@@ -51,6 +51,7 @@ class CcPlayoutHistoryTemplateTableMap extends TableMap
     public function buildRelations()
     {
         $this->addRelation('CcPlayoutHistoryTemplateField', 'CcPlayoutHistoryTemplateField', RelationMap::ONE_TO_MANY, array('id' => 'template_id', ), 'CASCADE', null, 'CcPlayoutHistoryTemplateFields');
+        $this->addRelation('ThirdPartyTrackReferences', 'ThirdPartyTrackReferences', RelationMap::ONE_TO_MANY, array('id' => 'file_id', ), 'CASCADE', null, 'ThirdPartyTrackReferencess');
     } // buildRelations()
 
 } // CcPlayoutHistoryTemplateTableMap
diff --git a/airtime_mvc/application/models/airtime/map/CcShowInstancesTableMap.php b/airtime_mvc/application/models/airtime/map/CcShowInstancesTableMap.php
index 0b22cef8f..0a52454dc 100644
--- a/airtime_mvc/application/models/airtime/map/CcShowInstancesTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CcShowInstancesTableMap.php
@@ -40,7 +40,7 @@ class CcShowInstancesTableMap extends TableMap
         $this->setPrimaryKeyMethodInfo('cc_show_instances_id_seq');
         // columns
         $this->addPrimaryKey('id', 'DbId', 'INTEGER', true, null, null);
-        $this->addColumn('description', 'DbDescription', 'VARCHAR', false, 512, '');
+        $this->addColumn('description', 'DbDescription', 'VARCHAR', false, 8192, '');
         $this->addColumn('starts', 'DbStarts', 'TIMESTAMP', true, null, null);
         $this->addColumn('ends', 'DbEnds', 'TIMESTAMP', true, null, null);
         $this->addForeignKey('show_id', 'DbShowId', 'INTEGER', 'cc_show', 'id', true, null, null);
diff --git a/airtime_mvc/application/models/airtime/map/CcShowTableMap.php b/airtime_mvc/application/models/airtime/map/CcShowTableMap.php
index c7442c83a..73ba89fac 100644
--- a/airtime_mvc/application/models/airtime/map/CcShowTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CcShowTableMap.php
@@ -43,7 +43,7 @@ class CcShowTableMap extends TableMap
         $this->addColumn('name', 'DbName', 'VARCHAR', true, 255, '');
         $this->addColumn('url', 'DbUrl', 'VARCHAR', false, 255, '');
         $this->addColumn('genre', 'DbGenre', 'VARCHAR', false, 255, '');
-        $this->addColumn('description', 'DbDescription', 'VARCHAR', false, 512, null);
+        $this->addColumn('description', 'DbDescription', 'VARCHAR', false, 8192, null);
         $this->addColumn('color', 'DbColor', 'VARCHAR', false, 6, null);
         $this->addColumn('background_color', 'DbBackgroundColor', 'VARCHAR', false, 6, null);
         $this->addColumn('live_stream_using_airtime_auth', 'DbLiveStreamUsingAirtimeAuth', 'BOOLEAN', false, null, false);
diff --git a/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php b/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
new file mode 100644
index 000000000..bf80e6cd1
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
@@ -0,0 +1,58 @@
+<?php
+
+
+
+/**
+ * This class defines the structure of the 'third_party_track_references' table.
+ *
+ *
+ *
+ * This map class is used by Propel to do runtime db structure discovery.
+ * For example, the createSelectSql() method checks the type of a given column used in an
+ * ORDER BY clause to know whether it needs to apply SQL to make the ORDER BY case-insensitive
+ * (i.e. if it's a text column type).
+ *
+ * @package    propel.generator.airtime.map
+ */
+class ThirdPartyTrackReferencesTableMap extends TableMap
+{
+
+    /**
+     * The (dot-path) name of this class
+     */
+    const CLASS_NAME = 'airtime.map.ThirdPartyTrackReferencesTableMap';
+
+    /**
+     * Initialize the table attributes, columns and validators
+     * Relations are not initialized by this method since they are lazy loaded
+     *
+     * @return void
+     * @throws PropelException
+     */
+    public function initialize()
+    {
+        // attributes
+        $this->setName('third_party_track_references');
+        $this->setPhpName('ThirdPartyTrackReferences');
+        $this->setClassname('ThirdPartyTrackReferences');
+        $this->setPackage('airtime');
+        $this->setUseIdGenerator(true);
+        $this->setPrimaryKeyMethodInfo('third_party_track_references_id_seq');
+        // columns
+        $this->addPrimaryKey('id', 'DbId', 'INTEGER', true, null, null);
+        $this->addColumn('service', 'DbService', 'VARCHAR', true, 512, null);
+        $this->addColumn('foreign_id', 'DbForeignId', 'INTEGER', true, null, null);
+        $this->addForeignKey('file_id', 'DbFileId', 'INTEGER', 'cc_playout_history_template', 'id', true, null, null);
+        $this->addColumn('status', 'DbStatus', 'VARCHAR', true, 256, null);
+        // validators
+    } // initialize()
+
+    /**
+     * Build the RelationMap objects for this table relationships
+     */
+    public function buildRelations()
+    {
+        $this->addRelation('CcPlayoutHistoryTemplate', 'CcPlayoutHistoryTemplate', RelationMap::MANY_TO_ONE, array('file_id' => 'id', ), 'CASCADE', null);
+    } // buildRelations()
+
+} // ThirdPartyTrackReferencesTableMap
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php
index 6a2b9230f..a78619c7c 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php
@@ -53,6 +53,12 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
     protected $collCcPlayoutHistoryTemplateFields;
     protected $collCcPlayoutHistoryTemplateFieldsPartial;
 
+    /**
+     * @var        PropelObjectCollection|ThirdPartyTrackReferences[] Collection to store aggregation of ThirdPartyTrackReferences objects.
+     */
+    protected $collThirdPartyTrackReferencess;
+    protected $collThirdPartyTrackReferencessPartial;
+
     /**
      * Flag to prevent endless save loop, if this object is referenced
      * by another object which falls in this transaction.
@@ -79,6 +85,12 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
      */
     protected $ccPlayoutHistoryTemplateFieldsScheduledForDeletion = null;
 
+    /**
+     * An array of objects scheduled for deletion.
+     * @var		PropelObjectCollection
+     */
+    protected $thirdPartyTrackReferencessScheduledForDeletion = null;
+
     /**
      * Get the [id] column value.
      *
@@ -283,6 +295,8 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
 
             $this->collCcPlayoutHistoryTemplateFields = null;
 
+            $this->collThirdPartyTrackReferencess = null;
+
         } // if (deep)
     }
 
@@ -424,6 +438,23 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                 }
             }
 
+            if ($this->thirdPartyTrackReferencessScheduledForDeletion !== null) {
+                if (!$this->thirdPartyTrackReferencessScheduledForDeletion->isEmpty()) {
+                    ThirdPartyTrackReferencesQuery::create()
+                        ->filterByPrimaryKeys($this->thirdPartyTrackReferencessScheduledForDeletion->getPrimaryKeys(false))
+                        ->delete($con);
+                    $this->thirdPartyTrackReferencessScheduledForDeletion = null;
+                }
+            }
+
+            if ($this->collThirdPartyTrackReferencess !== null) {
+                foreach ($this->collThirdPartyTrackReferencess as $referrerFK) {
+                    if (!$referrerFK->isDeleted() && ($referrerFK->isNew() || $referrerFK->isModified())) {
+                        $affectedRows += $referrerFK->save($con);
+                    }
+                }
+            }
+
             $this->alreadyInSave = false;
 
         }
@@ -589,6 +620,14 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                     }
                 }
 
+                if ($this->collThirdPartyTrackReferencess !== null) {
+                    foreach ($this->collThirdPartyTrackReferencess as $referrerFK) {
+                        if (!$referrerFK->validate($columns)) {
+                            $failureMap = array_merge($failureMap, $referrerFK->getValidationFailures());
+                        }
+                    }
+                }
+
 
             $this->alreadyInValidation = false;
         }
@@ -675,6 +714,9 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
             if (null !== $this->collCcPlayoutHistoryTemplateFields) {
                 $result['CcPlayoutHistoryTemplateFields'] = $this->collCcPlayoutHistoryTemplateFields->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
             }
+            if (null !== $this->collThirdPartyTrackReferencess) {
+                $result['ThirdPartyTrackReferencess'] = $this->collThirdPartyTrackReferencess->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
+            }
         }
 
         return $result;
@@ -838,6 +880,12 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                 }
             }
 
+            foreach ($this->getThirdPartyTrackReferencess() as $relObj) {
+                if ($relObj !== $this) {  // ensure that we don't try to copy a reference to ourselves
+                    $copyObj->addThirdPartyTrackReferences($relObj->copy($deepCopy));
+                }
+            }
+
             //unflag object copy
             $this->startCopy = false;
         } // if ($deepCopy)
@@ -902,6 +950,9 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
         if ('CcPlayoutHistoryTemplateField' == $relationName) {
             $this->initCcPlayoutHistoryTemplateFields();
         }
+        if ('ThirdPartyTrackReferences' == $relationName) {
+            $this->initThirdPartyTrackReferencess();
+        }
     }
 
     /**
@@ -1129,6 +1180,231 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
         return $this;
     }
 
+    /**
+     * Clears out the collThirdPartyTrackReferencess collection
+     *
+     * This does not modify the database; however, it will remove any associated objects, causing
+     * them to be refetched by subsequent calls to accessor method.
+     *
+     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
+     * @see        addThirdPartyTrackReferencess()
+     */
+    public function clearThirdPartyTrackReferencess()
+    {
+        $this->collThirdPartyTrackReferencess = null; // important to set this to null since that means it is uninitialized
+        $this->collThirdPartyTrackReferencessPartial = null;
+
+        return $this;
+    }
+
+    /**
+     * reset is the collThirdPartyTrackReferencess collection loaded partially
+     *
+     * @return void
+     */
+    public function resetPartialThirdPartyTrackReferencess($v = true)
+    {
+        $this->collThirdPartyTrackReferencessPartial = $v;
+    }
+
+    /**
+     * Initializes the collThirdPartyTrackReferencess collection.
+     *
+     * By default this just sets the collThirdPartyTrackReferencess collection to an empty array (like clearcollThirdPartyTrackReferencess());
+     * however, you may wish to override this method in your stub class to provide setting appropriate
+     * to your application -- for example, setting the initial array to the values stored in database.
+     *
+     * @param boolean $overrideExisting If set to true, the method call initializes
+     *                                        the collection even if it is not empty
+     *
+     * @return void
+     */
+    public function initThirdPartyTrackReferencess($overrideExisting = true)
+    {
+        if (null !== $this->collThirdPartyTrackReferencess && !$overrideExisting) {
+            return;
+        }
+        $this->collThirdPartyTrackReferencess = new PropelObjectCollection();
+        $this->collThirdPartyTrackReferencess->setModel('ThirdPartyTrackReferences');
+    }
+
+    /**
+     * Gets an array of ThirdPartyTrackReferences objects which contain a foreign key that references this object.
+     *
+     * If the $criteria is not null, it is used to always fetch the results from the database.
+     * Otherwise the results are fetched from the database the first time, then cached.
+     * Next time the same method is called without $criteria, the cached collection is returned.
+     * If this CcPlayoutHistoryTemplate is new, it will return
+     * an empty collection or the current collection; the criteria is ignored on a new object.
+     *
+     * @param Criteria $criteria optional Criteria object to narrow the query
+     * @param PropelPDO $con optional connection object
+     * @return PropelObjectCollection|ThirdPartyTrackReferences[] List of ThirdPartyTrackReferences objects
+     * @throws PropelException
+     */
+    public function getThirdPartyTrackReferencess($criteria = null, PropelPDO $con = null)
+    {
+        $partial = $this->collThirdPartyTrackReferencessPartial && !$this->isNew();
+        if (null === $this->collThirdPartyTrackReferencess || null !== $criteria  || $partial) {
+            if ($this->isNew() && null === $this->collThirdPartyTrackReferencess) {
+                // return empty collection
+                $this->initThirdPartyTrackReferencess();
+            } else {
+                $collThirdPartyTrackReferencess = ThirdPartyTrackReferencesQuery::create(null, $criteria)
+                    ->filterByCcPlayoutHistoryTemplate($this)
+                    ->find($con);
+                if (null !== $criteria) {
+                    if (false !== $this->collThirdPartyTrackReferencessPartial && count($collThirdPartyTrackReferencess)) {
+                      $this->initThirdPartyTrackReferencess(false);
+
+                      foreach ($collThirdPartyTrackReferencess as $obj) {
+                        if (false == $this->collThirdPartyTrackReferencess->contains($obj)) {
+                          $this->collThirdPartyTrackReferencess->append($obj);
+                        }
+                      }
+
+                      $this->collThirdPartyTrackReferencessPartial = true;
+                    }
+
+                    $collThirdPartyTrackReferencess->getInternalIterator()->rewind();
+
+                    return $collThirdPartyTrackReferencess;
+                }
+
+                if ($partial && $this->collThirdPartyTrackReferencess) {
+                    foreach ($this->collThirdPartyTrackReferencess as $obj) {
+                        if ($obj->isNew()) {
+                            $collThirdPartyTrackReferencess[] = $obj;
+                        }
+                    }
+                }
+
+                $this->collThirdPartyTrackReferencess = $collThirdPartyTrackReferencess;
+                $this->collThirdPartyTrackReferencessPartial = false;
+            }
+        }
+
+        return $this->collThirdPartyTrackReferencess;
+    }
+
+    /**
+     * Sets a collection of ThirdPartyTrackReferences objects related by a one-to-many relationship
+     * to the current object.
+     * It will also schedule objects for deletion based on a diff between old objects (aka persisted)
+     * and new objects from the given Propel collection.
+     *
+     * @param PropelCollection $thirdPartyTrackReferencess A Propel collection.
+     * @param PropelPDO $con Optional connection object
+     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
+     */
+    public function setThirdPartyTrackReferencess(PropelCollection $thirdPartyTrackReferencess, PropelPDO $con = null)
+    {
+        $thirdPartyTrackReferencessToDelete = $this->getThirdPartyTrackReferencess(new Criteria(), $con)->diff($thirdPartyTrackReferencess);
+
+
+        $this->thirdPartyTrackReferencessScheduledForDeletion = $thirdPartyTrackReferencessToDelete;
+
+        foreach ($thirdPartyTrackReferencessToDelete as $thirdPartyTrackReferencesRemoved) {
+            $thirdPartyTrackReferencesRemoved->setCcPlayoutHistoryTemplate(null);
+        }
+
+        $this->collThirdPartyTrackReferencess = null;
+        foreach ($thirdPartyTrackReferencess as $thirdPartyTrackReferences) {
+            $this->addThirdPartyTrackReferences($thirdPartyTrackReferences);
+        }
+
+        $this->collThirdPartyTrackReferencess = $thirdPartyTrackReferencess;
+        $this->collThirdPartyTrackReferencessPartial = false;
+
+        return $this;
+    }
+
+    /**
+     * Returns the number of related ThirdPartyTrackReferences objects.
+     *
+     * @param Criteria $criteria
+     * @param boolean $distinct
+     * @param PropelPDO $con
+     * @return int             Count of related ThirdPartyTrackReferences objects.
+     * @throws PropelException
+     */
+    public function countThirdPartyTrackReferencess(Criteria $criteria = null, $distinct = false, PropelPDO $con = null)
+    {
+        $partial = $this->collThirdPartyTrackReferencessPartial && !$this->isNew();
+        if (null === $this->collThirdPartyTrackReferencess || null !== $criteria || $partial) {
+            if ($this->isNew() && null === $this->collThirdPartyTrackReferencess) {
+                return 0;
+            }
+
+            if ($partial && !$criteria) {
+                return count($this->getThirdPartyTrackReferencess());
+            }
+            $query = ThirdPartyTrackReferencesQuery::create(null, $criteria);
+            if ($distinct) {
+                $query->distinct();
+            }
+
+            return $query
+                ->filterByCcPlayoutHistoryTemplate($this)
+                ->count($con);
+        }
+
+        return count($this->collThirdPartyTrackReferencess);
+    }
+
+    /**
+     * Method called to associate a ThirdPartyTrackReferences object to this object
+     * through the ThirdPartyTrackReferences foreign key attribute.
+     *
+     * @param    ThirdPartyTrackReferences $l ThirdPartyTrackReferences
+     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
+     */
+    public function addThirdPartyTrackReferences(ThirdPartyTrackReferences $l)
+    {
+        if ($this->collThirdPartyTrackReferencess === null) {
+            $this->initThirdPartyTrackReferencess();
+            $this->collThirdPartyTrackReferencessPartial = true;
+        }
+
+        if (!in_array($l, $this->collThirdPartyTrackReferencess->getArrayCopy(), true)) { // only add it if the **same** object is not already associated
+            $this->doAddThirdPartyTrackReferences($l);
+
+            if ($this->thirdPartyTrackReferencessScheduledForDeletion and $this->thirdPartyTrackReferencessScheduledForDeletion->contains($l)) {
+                $this->thirdPartyTrackReferencessScheduledForDeletion->remove($this->thirdPartyTrackReferencessScheduledForDeletion->search($l));
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param	ThirdPartyTrackReferences $thirdPartyTrackReferences The thirdPartyTrackReferences object to add.
+     */
+    protected function doAddThirdPartyTrackReferences($thirdPartyTrackReferences)
+    {
+        $this->collThirdPartyTrackReferencess[]= $thirdPartyTrackReferences;
+        $thirdPartyTrackReferences->setCcPlayoutHistoryTemplate($this);
+    }
+
+    /**
+     * @param	ThirdPartyTrackReferences $thirdPartyTrackReferences The thirdPartyTrackReferences object to remove.
+     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
+     */
+    public function removeThirdPartyTrackReferences($thirdPartyTrackReferences)
+    {
+        if ($this->getThirdPartyTrackReferencess()->contains($thirdPartyTrackReferences)) {
+            $this->collThirdPartyTrackReferencess->remove($this->collThirdPartyTrackReferencess->search($thirdPartyTrackReferences));
+            if (null === $this->thirdPartyTrackReferencessScheduledForDeletion) {
+                $this->thirdPartyTrackReferencessScheduledForDeletion = clone $this->collThirdPartyTrackReferencess;
+                $this->thirdPartyTrackReferencessScheduledForDeletion->clear();
+            }
+            $this->thirdPartyTrackReferencessScheduledForDeletion[]= clone $thirdPartyTrackReferences;
+            $thirdPartyTrackReferences->setCcPlayoutHistoryTemplate(null);
+        }
+
+        return $this;
+    }
+
     /**
      * Clears the current object and sets all attributes to their default values
      */
@@ -1164,6 +1440,11 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                     $o->clearAllReferences($deep);
                 }
             }
+            if ($this->collThirdPartyTrackReferencess) {
+                foreach ($this->collThirdPartyTrackReferencess as $o) {
+                    $o->clearAllReferences($deep);
+                }
+            }
 
             $this->alreadyInClearAllReferencesDeep = false;
         } // if ($deep)
@@ -1172,6 +1453,10 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
             $this->collCcPlayoutHistoryTemplateFields->clearIterator();
         }
         $this->collCcPlayoutHistoryTemplateFields = null;
+        if ($this->collThirdPartyTrackReferencess instanceof PropelCollection) {
+            $this->collThirdPartyTrackReferencess->clearIterator();
+        }
+        $this->collThirdPartyTrackReferencess = null;
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php
index 89c7cdc9d..f30c447fe 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php
@@ -368,6 +368,9 @@ abstract class BaseCcPlayoutHistoryTemplatePeer
         // Invalidate objects in CcPlayoutHistoryTemplateFieldPeer instance pool,
         // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
         CcPlayoutHistoryTemplateFieldPeer::clearInstancePool();
+        // Invalidate objects in ThirdPartyTrackReferencesPeer instance pool,
+        // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
+        ThirdPartyTrackReferencesPeer::clearInstancePool();
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php
index 262b0ee2c..34ed52def 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php
@@ -22,6 +22,10 @@
  * @method CcPlayoutHistoryTemplateQuery rightJoinCcPlayoutHistoryTemplateField($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CcPlayoutHistoryTemplateField relation
  * @method CcPlayoutHistoryTemplateQuery innerJoinCcPlayoutHistoryTemplateField($relationAlias = null) Adds a INNER JOIN clause to the query using the CcPlayoutHistoryTemplateField relation
  *
+ * @method CcPlayoutHistoryTemplateQuery leftJoinThirdPartyTrackReferences($relationAlias = null) Adds a LEFT JOIN clause to the query using the ThirdPartyTrackReferences relation
+ * @method CcPlayoutHistoryTemplateQuery rightJoinThirdPartyTrackReferences($relationAlias = null) Adds a RIGHT JOIN clause to the query using the ThirdPartyTrackReferences relation
+ * @method CcPlayoutHistoryTemplateQuery innerJoinThirdPartyTrackReferences($relationAlias = null) Adds a INNER JOIN clause to the query using the ThirdPartyTrackReferences relation
+ *
  * @method CcPlayoutHistoryTemplate findOne(PropelPDO $con = null) Return the first CcPlayoutHistoryTemplate matching the query
  * @method CcPlayoutHistoryTemplate findOneOrCreate(PropelPDO $con = null) Return the first CcPlayoutHistoryTemplate matching the query, or a new CcPlayoutHistoryTemplate object populated from the query conditions when no match is found
  *
@@ -401,6 +405,80 @@ abstract class BaseCcPlayoutHistoryTemplateQuery extends ModelCriteria
             ->useQuery($relationAlias ? $relationAlias : 'CcPlayoutHistoryTemplateField', 'CcPlayoutHistoryTemplateFieldQuery');
     }
 
+    /**
+     * Filter the query by a related ThirdPartyTrackReferences object
+     *
+     * @param   ThirdPartyTrackReferences|PropelObjectCollection $thirdPartyTrackReferences  the related object to use as filter
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return                 CcPlayoutHistoryTemplateQuery The current query, for fluid interface
+     * @throws PropelException - if the provided filter is invalid.
+     */
+    public function filterByThirdPartyTrackReferences($thirdPartyTrackReferences, $comparison = null)
+    {
+        if ($thirdPartyTrackReferences instanceof ThirdPartyTrackReferences) {
+            return $this
+                ->addUsingAlias(CcPlayoutHistoryTemplatePeer::ID, $thirdPartyTrackReferences->getDbFileId(), $comparison);
+        } elseif ($thirdPartyTrackReferences instanceof PropelObjectCollection) {
+            return $this
+                ->useThirdPartyTrackReferencesQuery()
+                ->filterByPrimaryKeys($thirdPartyTrackReferences->getPrimaryKeys())
+                ->endUse();
+        } else {
+            throw new PropelException('filterByThirdPartyTrackReferences() only accepts arguments of type ThirdPartyTrackReferences or PropelCollection');
+        }
+    }
+
+    /**
+     * Adds a JOIN clause to the query using the ThirdPartyTrackReferences relation
+     *
+     * @param     string $relationAlias optional alias for the relation
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return CcPlayoutHistoryTemplateQuery The current query, for fluid interface
+     */
+    public function joinThirdPartyTrackReferences($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        $tableMap = $this->getTableMap();
+        $relationMap = $tableMap->getRelation('ThirdPartyTrackReferences');
+
+        // create a ModelJoin object for this join
+        $join = new ModelJoin();
+        $join->setJoinType($joinType);
+        $join->setRelationMap($relationMap, $this->useAliasInSQL ? $this->getModelAlias() : null, $relationAlias);
+        if ($previousJoin = $this->getPreviousJoin()) {
+            $join->setPreviousJoin($previousJoin);
+        }
+
+        // add the ModelJoin to the current object
+        if ($relationAlias) {
+            $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
+            $this->addJoinObject($join, $relationAlias);
+        } else {
+            $this->addJoinObject($join, 'ThirdPartyTrackReferences');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Use the ThirdPartyTrackReferences relation ThirdPartyTrackReferences object
+     *
+     * @see       useQuery()
+     *
+     * @param     string $relationAlias optional alias for the relation,
+     *                                   to be used as main alias in the secondary query
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return   ThirdPartyTrackReferencesQuery A secondary query class using the current class as primary query
+     */
+    public function useThirdPartyTrackReferencesQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        return $this
+            ->joinThirdPartyTrackReferences($relationAlias, $joinType)
+            ->useQuery($relationAlias ? $relationAlias : 'ThirdPartyTrackReferences', 'ThirdPartyTrackReferencesQuery');
+    }
+
     /**
      * Exclude object from result
      *
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
new file mode 100644
index 000000000..b94085171
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
@@ -0,0 +1,1107 @@
+<?php
+
+
+/**
+ * Base class that represents a row from the 'third_party_track_references' table.
+ *
+ *
+ *
+ * @package    propel.generator.airtime.om
+ */
+abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persistent
+{
+    /**
+     * Peer class name
+     */
+    const PEER = 'ThirdPartyTrackReferencesPeer';
+
+    /**
+     * The Peer class.
+     * Instance provides a convenient way of calling static methods on a class
+     * that calling code may not be able to identify.
+     * @var        ThirdPartyTrackReferencesPeer
+     */
+    protected static $peer;
+
+    /**
+     * The flag var to prevent infinite loop in deep copy
+     * @var       boolean
+     */
+    protected $startCopy = false;
+
+    /**
+     * The value for the id field.
+     * @var        int
+     */
+    protected $id;
+
+    /**
+     * The value for the service field.
+     * @var        string
+     */
+    protected $service;
+
+    /**
+     * The value for the foreign_id field.
+     * @var        int
+     */
+    protected $foreign_id;
+
+    /**
+     * The value for the file_id field.
+     * @var        int
+     */
+    protected $file_id;
+
+    /**
+     * The value for the status field.
+     * @var        string
+     */
+    protected $status;
+
+    /**
+     * @var        CcPlayoutHistoryTemplate
+     */
+    protected $aCcPlayoutHistoryTemplate;
+
+    /**
+     * Flag to prevent endless save loop, if this object is referenced
+     * by another object which falls in this transaction.
+     * @var        boolean
+     */
+    protected $alreadyInSave = false;
+
+    /**
+     * Flag to prevent endless validation loop, if this object is referenced
+     * by another object which falls in this transaction.
+     * @var        boolean
+     */
+    protected $alreadyInValidation = false;
+
+    /**
+     * Flag to prevent endless clearAllReferences($deep=true) loop, if this object is referenced
+     * @var        boolean
+     */
+    protected $alreadyInClearAllReferencesDeep = false;
+
+    /**
+     * Get the [id] column value.
+     *
+     * @return int
+     */
+    public function getDbId()
+    {
+
+        return $this->id;
+    }
+
+    /**
+     * Get the [service] column value.
+     *
+     * @return string
+     */
+    public function getDbService()
+    {
+
+        return $this->service;
+    }
+
+    /**
+     * Get the [foreign_id] column value.
+     *
+     * @return int
+     */
+    public function getDbForeignId()
+    {
+
+        return $this->foreign_id;
+    }
+
+    /**
+     * Get the [file_id] column value.
+     *
+     * @return int
+     */
+    public function getDbFileId()
+    {
+
+        return $this->file_id;
+    }
+
+    /**
+     * Get the [status] column value.
+     *
+     * @return string
+     */
+    public function getDbStatus()
+    {
+
+        return $this->status;
+    }
+
+    /**
+     * Set the value of [id] column.
+     *
+     * @param  int $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbId($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (int) $v;
+        }
+
+        if ($this->id !== $v) {
+            $this->id = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::ID;
+        }
+
+
+        return $this;
+    } // setDbId()
+
+    /**
+     * Set the value of [service] column.
+     *
+     * @param  string $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbService($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->service !== $v) {
+            $this->service = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::SERVICE;
+        }
+
+
+        return $this;
+    } // setDbService()
+
+    /**
+     * Set the value of [foreign_id] column.
+     *
+     * @param  int $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbForeignId($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (int) $v;
+        }
+
+        if ($this->foreign_id !== $v) {
+            $this->foreign_id = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::FOREIGN_ID;
+        }
+
+
+        return $this;
+    } // setDbForeignId()
+
+    /**
+     * Set the value of [file_id] column.
+     *
+     * @param  int $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbFileId($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (int) $v;
+        }
+
+        if ($this->file_id !== $v) {
+            $this->file_id = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::FILE_ID;
+        }
+
+        if ($this->aCcPlayoutHistoryTemplate !== null && $this->aCcPlayoutHistoryTemplate->getDbId() !== $v) {
+            $this->aCcPlayoutHistoryTemplate = null;
+        }
+
+
+        return $this;
+    } // setDbFileId()
+
+    /**
+     * Set the value of [status] column.
+     *
+     * @param  string $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbStatus($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->status !== $v) {
+            $this->status = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::STATUS;
+        }
+
+
+        return $this;
+    } // setDbStatus()
+
+    /**
+     * Indicates whether the columns in this object are only set to default values.
+     *
+     * This method can be used in conjunction with isModified() to indicate whether an object is both
+     * modified _and_ has some values set which are non-default.
+     *
+     * @return boolean Whether the columns in this object are only been set with default values.
+     */
+    public function hasOnlyDefaultValues()
+    {
+        // otherwise, everything was equal, so return true
+        return true;
+    } // hasOnlyDefaultValues()
+
+    /**
+     * Hydrates (populates) the object variables with values from the database resultset.
+     *
+     * An offset (0-based "start column") is specified so that objects can be hydrated
+     * with a subset of the columns in the resultset rows.  This is needed, for example,
+     * for results of JOIN queries where the resultset row includes columns from two or
+     * more tables.
+     *
+     * @param array $row The row returned by PDOStatement->fetch(PDO::FETCH_NUM)
+     * @param int $startcol 0-based offset column which indicates which resultset column to start with.
+     * @param boolean $rehydrate Whether this object is being re-hydrated from the database.
+     * @return int             next starting column
+     * @throws PropelException - Any caught Exception will be rewrapped as a PropelException.
+     */
+    public function hydrate($row, $startcol = 0, $rehydrate = false)
+    {
+        try {
+
+            $this->id = ($row[$startcol + 0] !== null) ? (int) $row[$startcol + 0] : null;
+            $this->service = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null;
+            $this->foreign_id = ($row[$startcol + 2] !== null) ? (int) $row[$startcol + 2] : null;
+            $this->file_id = ($row[$startcol + 3] !== null) ? (int) $row[$startcol + 3] : null;
+            $this->status = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->resetModified();
+
+            $this->setNew(false);
+
+            if ($rehydrate) {
+                $this->ensureConsistency();
+            }
+            $this->postHydrate($row, $startcol, $rehydrate);
+
+            return $startcol + 5; // 5 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS.
+
+        } catch (Exception $e) {
+            throw new PropelException("Error populating ThirdPartyTrackReferences object", $e);
+        }
+    }
+
+    /**
+     * Checks and repairs the internal consistency of the object.
+     *
+     * This method is executed after an already-instantiated object is re-hydrated
+     * from the database.  It exists to check any foreign keys to make sure that
+     * the objects related to the current object are correct based on foreign key.
+     *
+     * You can override this method in the stub class, but you should always invoke
+     * the base method from the overridden method (i.e. parent::ensureConsistency()),
+     * in case your model changes.
+     *
+     * @throws PropelException
+     */
+    public function ensureConsistency()
+    {
+
+        if ($this->aCcPlayoutHistoryTemplate !== null && $this->file_id !== $this->aCcPlayoutHistoryTemplate->getDbId()) {
+            $this->aCcPlayoutHistoryTemplate = null;
+        }
+    } // ensureConsistency
+
+    /**
+     * Reloads this object from datastore based on primary key and (optionally) resets all associated objects.
+     *
+     * This will only work if the object has been saved and has a valid primary key set.
+     *
+     * @param boolean $deep (optional) Whether to also de-associated any related objects.
+     * @param PropelPDO $con (optional) The PropelPDO connection to use.
+     * @return void
+     * @throws PropelException - if this object is deleted, unsaved or doesn't have pk match in db
+     */
+    public function reload($deep = false, PropelPDO $con = null)
+    {
+        if ($this->isDeleted()) {
+            throw new PropelException("Cannot reload a deleted object.");
+        }
+
+        if ($this->isNew()) {
+            throw new PropelException("Cannot reload an unsaved object.");
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        // We don't need to alter the object instance pool; we're just modifying this instance
+        // already in the pool.
+
+        $stmt = ThirdPartyTrackReferencesPeer::doSelectStmt($this->buildPkeyCriteria(), $con);
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+        $stmt->closeCursor();
+        if (!$row) {
+            throw new PropelException('Cannot find matching row in the database to reload object values.');
+        }
+        $this->hydrate($row, 0, true); // rehydrate
+
+        if ($deep) {  // also de-associate any related objects?
+
+            $this->aCcPlayoutHistoryTemplate = null;
+        } // if (deep)
+    }
+
+    /**
+     * Removes this object from datastore and sets delete attribute.
+     *
+     * @param PropelPDO $con
+     * @return void
+     * @throws PropelException
+     * @throws Exception
+     * @see        BaseObject::setDeleted()
+     * @see        BaseObject::isDeleted()
+     */
+    public function delete(PropelPDO $con = null)
+    {
+        if ($this->isDeleted()) {
+            throw new PropelException("This object has already been deleted.");
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        $con->beginTransaction();
+        try {
+            $deleteQuery = ThirdPartyTrackReferencesQuery::create()
+                ->filterByPrimaryKey($this->getPrimaryKey());
+            $ret = $this->preDelete($con);
+            if ($ret) {
+                $deleteQuery->delete($con);
+                $this->postDelete($con);
+                $con->commit();
+                $this->setDeleted(true);
+            } else {
+                $con->commit();
+            }
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Persists this object to the database.
+     *
+     * If the object is new, it inserts it; otherwise an update is performed.
+     * All modified related objects will also be persisted in the doSave()
+     * method.  This method wraps all precipitate database operations in a
+     * single transaction.
+     *
+     * @param PropelPDO $con
+     * @return int             The number of rows affected by this insert/update and any referring fk objects' save() operations.
+     * @throws PropelException
+     * @throws Exception
+     * @see        doSave()
+     */
+    public function save(PropelPDO $con = null)
+    {
+        if ($this->isDeleted()) {
+            throw new PropelException("You cannot save an object that has been deleted.");
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        $con->beginTransaction();
+        $isInsert = $this->isNew();
+        try {
+            $ret = $this->preSave($con);
+            if ($isInsert) {
+                $ret = $ret && $this->preInsert($con);
+            } else {
+                $ret = $ret && $this->preUpdate($con);
+            }
+            if ($ret) {
+                $affectedRows = $this->doSave($con);
+                if ($isInsert) {
+                    $this->postInsert($con);
+                } else {
+                    $this->postUpdate($con);
+                }
+                $this->postSave($con);
+                ThirdPartyTrackReferencesPeer::addInstanceToPool($this);
+            } else {
+                $affectedRows = 0;
+            }
+            $con->commit();
+
+            return $affectedRows;
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Performs the work of inserting or updating the row in the database.
+     *
+     * If the object is new, it inserts it; otherwise an update is performed.
+     * All related objects are also updated in this method.
+     *
+     * @param PropelPDO $con
+     * @return int             The number of rows affected by this insert/update and any referring fk objects' save() operations.
+     * @throws PropelException
+     * @see        save()
+     */
+    protected function doSave(PropelPDO $con)
+    {
+        $affectedRows = 0; // initialize var to track total num of affected rows
+        if (!$this->alreadyInSave) {
+            $this->alreadyInSave = true;
+
+            // We call the save method on the following object(s) if they
+            // were passed to this object by their corresponding set
+            // method.  This object relates to these object(s) by a
+            // foreign key reference.
+
+            if ($this->aCcPlayoutHistoryTemplate !== null) {
+                if ($this->aCcPlayoutHistoryTemplate->isModified() || $this->aCcPlayoutHistoryTemplate->isNew()) {
+                    $affectedRows += $this->aCcPlayoutHistoryTemplate->save($con);
+                }
+                $this->setCcPlayoutHistoryTemplate($this->aCcPlayoutHistoryTemplate);
+            }
+
+            if ($this->isNew() || $this->isModified()) {
+                // persist changes
+                if ($this->isNew()) {
+                    $this->doInsert($con);
+                } else {
+                    $this->doUpdate($con);
+                }
+                $affectedRows += 1;
+                $this->resetModified();
+            }
+
+            $this->alreadyInSave = false;
+
+        }
+
+        return $affectedRows;
+    } // doSave()
+
+    /**
+     * Insert the row in the database.
+     *
+     * @param PropelPDO $con
+     *
+     * @throws PropelException
+     * @see        doSave()
+     */
+    protected function doInsert(PropelPDO $con)
+    {
+        $modifiedColumns = array();
+        $index = 0;
+
+        $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::ID;
+        if (null !== $this->id) {
+            throw new PropelException('Cannot insert a value for auto-increment primary key (' . ThirdPartyTrackReferencesPeer::ID . ')');
+        }
+        if (null === $this->id) {
+            try {
+                $stmt = $con->query("SELECT nextval('third_party_track_references_id_seq')");
+                $row = $stmt->fetch(PDO::FETCH_NUM);
+                $this->id = $row[0];
+            } catch (Exception $e) {
+                throw new PropelException('Unable to get sequence id.', $e);
+            }
+        }
+
+
+         // check the columns in natural order for more readable SQL queries
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::ID)) {
+            $modifiedColumns[':p' . $index++]  = '"id"';
+        }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::SERVICE)) {
+            $modifiedColumns[':p' . $index++]  = '"service"';
+        }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FOREIGN_ID)) {
+            $modifiedColumns[':p' . $index++]  = '"foreign_id"';
+        }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FILE_ID)) {
+            $modifiedColumns[':p' . $index++]  = '"file_id"';
+        }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::STATUS)) {
+            $modifiedColumns[':p' . $index++]  = '"status"';
+        }
+
+        $sql = sprintf(
+            'INSERT INTO "third_party_track_references" (%s) VALUES (%s)',
+            implode(', ', $modifiedColumns),
+            implode(', ', array_keys($modifiedColumns))
+        );
+
+        try {
+            $stmt = $con->prepare($sql);
+            foreach ($modifiedColumns as $identifier => $columnName) {
+                switch ($columnName) {
+                    case '"id"':
+                        $stmt->bindValue($identifier, $this->id, PDO::PARAM_INT);
+                        break;
+                    case '"service"':
+                        $stmt->bindValue($identifier, $this->service, PDO::PARAM_STR);
+                        break;
+                    case '"foreign_id"':
+                        $stmt->bindValue($identifier, $this->foreign_id, PDO::PARAM_INT);
+                        break;
+                    case '"file_id"':
+                        $stmt->bindValue($identifier, $this->file_id, PDO::PARAM_INT);
+                        break;
+                    case '"status"':
+                        $stmt->bindValue($identifier, $this->status, PDO::PARAM_STR);
+                        break;
+                }
+            }
+            $stmt->execute();
+        } catch (Exception $e) {
+            Propel::log($e->getMessage(), Propel::LOG_ERR);
+            throw new PropelException(sprintf('Unable to execute INSERT statement [%s]', $sql), $e);
+        }
+
+        $this->setNew(false);
+    }
+
+    /**
+     * Update the row in the database.
+     *
+     * @param PropelPDO $con
+     *
+     * @see        doSave()
+     */
+    protected function doUpdate(PropelPDO $con)
+    {
+        $selectCriteria = $this->buildPkeyCriteria();
+        $valuesCriteria = $this->buildCriteria();
+        BasePeer::doUpdate($selectCriteria, $valuesCriteria, $con);
+    }
+
+    /**
+     * Array of ValidationFailed objects.
+     * @var        array ValidationFailed[]
+     */
+    protected $validationFailures = array();
+
+    /**
+     * Gets any ValidationFailed objects that resulted from last call to validate().
+     *
+     *
+     * @return array ValidationFailed[]
+     * @see        validate()
+     */
+    public function getValidationFailures()
+    {
+        return $this->validationFailures;
+    }
+
+    /**
+     * Validates the objects modified field values and all objects related to this table.
+     *
+     * If $columns is either a column name or an array of column names
+     * only those columns are validated.
+     *
+     * @param mixed $columns Column name or an array of column names.
+     * @return boolean Whether all columns pass validation.
+     * @see        doValidate()
+     * @see        getValidationFailures()
+     */
+    public function validate($columns = null)
+    {
+        $res = $this->doValidate($columns);
+        if ($res === true) {
+            $this->validationFailures = array();
+
+            return true;
+        }
+
+        $this->validationFailures = $res;
+
+        return false;
+    }
+
+    /**
+     * This function performs the validation work for complex object models.
+     *
+     * In addition to checking the current object, all related objects will
+     * also be validated.  If all pass then <code>true</code> is returned; otherwise
+     * an aggregated array of ValidationFailed objects will be returned.
+     *
+     * @param array $columns Array of column names to validate.
+     * @return mixed <code>true</code> if all validations pass; array of <code>ValidationFailed</code> objects otherwise.
+     */
+    protected function doValidate($columns = null)
+    {
+        if (!$this->alreadyInValidation) {
+            $this->alreadyInValidation = true;
+            $retval = null;
+
+            $failureMap = array();
+
+
+            // We call the validate method on the following object(s) if they
+            // were passed to this object by their corresponding set
+            // method.  This object relates to these object(s) by a
+            // foreign key reference.
+
+            if ($this->aCcPlayoutHistoryTemplate !== null) {
+                if (!$this->aCcPlayoutHistoryTemplate->validate($columns)) {
+                    $failureMap = array_merge($failureMap, $this->aCcPlayoutHistoryTemplate->getValidationFailures());
+                }
+            }
+
+
+            if (($retval = ThirdPartyTrackReferencesPeer::doValidate($this, $columns)) !== true) {
+                $failureMap = array_merge($failureMap, $retval);
+            }
+
+
+
+            $this->alreadyInValidation = false;
+        }
+
+        return (!empty($failureMap) ? $failureMap : true);
+    }
+
+    /**
+     * Retrieves a field from the object by name passed in as a string.
+     *
+     * @param string $name name
+     * @param string $type The type of fieldname the $name is of:
+     *               one of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *               BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     *               Defaults to BasePeer::TYPE_PHPNAME
+     * @return mixed Value of field.
+     */
+    public function getByName($name, $type = BasePeer::TYPE_PHPNAME)
+    {
+        $pos = ThirdPartyTrackReferencesPeer::translateFieldName($name, $type, BasePeer::TYPE_NUM);
+        $field = $this->getByPosition($pos);
+
+        return $field;
+    }
+
+    /**
+     * Retrieves a field from the object by Position as specified in the xml schema.
+     * Zero-based.
+     *
+     * @param int $pos position in xml schema
+     * @return mixed Value of field at $pos
+     */
+    public function getByPosition($pos)
+    {
+        switch ($pos) {
+            case 0:
+                return $this->getDbId();
+                break;
+            case 1:
+                return $this->getDbService();
+                break;
+            case 2:
+                return $this->getDbForeignId();
+                break;
+            case 3:
+                return $this->getDbFileId();
+                break;
+            case 4:
+                return $this->getDbStatus();
+                break;
+            default:
+                return null;
+                break;
+        } // switch()
+    }
+
+    /**
+     * Exports the object as an array.
+     *
+     * You can specify the key type of the array by passing one of the class
+     * type constants.
+     *
+     * @param     string  $keyType (optional) One of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME,
+     *                    BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     *                    Defaults to BasePeer::TYPE_PHPNAME.
+     * @param     boolean $includeLazyLoadColumns (optional) Whether to include lazy loaded columns. Defaults to true.
+     * @param     array $alreadyDumpedObjects List of objects to skip to avoid recursion
+     * @param     boolean $includeForeignObjects (optional) Whether to include hydrated related objects. Default to FALSE.
+     *
+     * @return array an associative array containing the field names (as keys) and field values
+     */
+    public function toArray($keyType = BasePeer::TYPE_PHPNAME, $includeLazyLoadColumns = true, $alreadyDumpedObjects = array(), $includeForeignObjects = false)
+    {
+        if (isset($alreadyDumpedObjects['ThirdPartyTrackReferences'][$this->getPrimaryKey()])) {
+            return '*RECURSION*';
+        }
+        $alreadyDumpedObjects['ThirdPartyTrackReferences'][$this->getPrimaryKey()] = true;
+        $keys = ThirdPartyTrackReferencesPeer::getFieldNames($keyType);
+        $result = array(
+            $keys[0] => $this->getDbId(),
+            $keys[1] => $this->getDbService(),
+            $keys[2] => $this->getDbForeignId(),
+            $keys[3] => $this->getDbFileId(),
+            $keys[4] => $this->getDbStatus(),
+        );
+        $virtualColumns = $this->virtualColumns;
+        foreach ($virtualColumns as $key => $virtualColumn) {
+            $result[$key] = $virtualColumn;
+        }
+
+        if ($includeForeignObjects) {
+            if (null !== $this->aCcPlayoutHistoryTemplate) {
+                $result['CcPlayoutHistoryTemplate'] = $this->aCcPlayoutHistoryTemplate->toArray($keyType, $includeLazyLoadColumns,  $alreadyDumpedObjects, true);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Sets a field from the object by name passed in as a string.
+     *
+     * @param string $name peer name
+     * @param mixed $value field value
+     * @param string $type The type of fieldname the $name is of:
+     *                     one of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *                     BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     *                     Defaults to BasePeer::TYPE_PHPNAME
+     * @return void
+     */
+    public function setByName($name, $value, $type = BasePeer::TYPE_PHPNAME)
+    {
+        $pos = ThirdPartyTrackReferencesPeer::translateFieldName($name, $type, BasePeer::TYPE_NUM);
+
+        $this->setByPosition($pos, $value);
+    }
+
+    /**
+     * Sets a field from the object by Position as specified in the xml schema.
+     * Zero-based.
+     *
+     * @param int $pos position in xml schema
+     * @param mixed $value field value
+     * @return void
+     */
+    public function setByPosition($pos, $value)
+    {
+        switch ($pos) {
+            case 0:
+                $this->setDbId($value);
+                break;
+            case 1:
+                $this->setDbService($value);
+                break;
+            case 2:
+                $this->setDbForeignId($value);
+                break;
+            case 3:
+                $this->setDbFileId($value);
+                break;
+            case 4:
+                $this->setDbStatus($value);
+                break;
+        } // switch()
+    }
+
+    /**
+     * Populates the object using an array.
+     *
+     * This is particularly useful when populating an object from one of the
+     * request arrays (e.g. $_POST).  This method goes through the column
+     * names, checking to see whether a matching key exists in populated
+     * array. If so the setByName() method is called for that column.
+     *
+     * You can specify the key type of the array by additionally passing one
+     * of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME,
+     * BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     * The default key type is the column's BasePeer::TYPE_PHPNAME
+     *
+     * @param array  $arr     An array to populate the object from.
+     * @param string $keyType The type of keys the array uses.
+     * @return void
+     */
+    public function fromArray($arr, $keyType = BasePeer::TYPE_PHPNAME)
+    {
+        $keys = ThirdPartyTrackReferencesPeer::getFieldNames($keyType);
+
+        if (array_key_exists($keys[0], $arr)) $this->setDbId($arr[$keys[0]]);
+        if (array_key_exists($keys[1], $arr)) $this->setDbService($arr[$keys[1]]);
+        if (array_key_exists($keys[2], $arr)) $this->setDbForeignId($arr[$keys[2]]);
+        if (array_key_exists($keys[3], $arr)) $this->setDbFileId($arr[$keys[3]]);
+        if (array_key_exists($keys[4], $arr)) $this->setDbStatus($arr[$keys[4]]);
+    }
+
+    /**
+     * Build a Criteria object containing the values of all modified columns in this object.
+     *
+     * @return Criteria The Criteria object containing all modified values.
+     */
+    public function buildCriteria()
+    {
+        $criteria = new Criteria(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::ID)) $criteria->add(ThirdPartyTrackReferencesPeer::ID, $this->id);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::SERVICE)) $criteria->add(ThirdPartyTrackReferencesPeer::SERVICE, $this->service);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FOREIGN_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $this->foreign_id);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FILE_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::FILE_ID, $this->file_id);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::STATUS)) $criteria->add(ThirdPartyTrackReferencesPeer::STATUS, $this->status);
+
+        return $criteria;
+    }
+
+    /**
+     * Builds a Criteria object containing the primary key for this object.
+     *
+     * Unlike buildCriteria() this method includes the primary key values regardless
+     * of whether or not they have been modified.
+     *
+     * @return Criteria The Criteria object containing value(s) for primary key(s).
+     */
+    public function buildPkeyCriteria()
+    {
+        $criteria = new Criteria(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+        $criteria->add(ThirdPartyTrackReferencesPeer::ID, $this->id);
+
+        return $criteria;
+    }
+
+    /**
+     * Returns the primary key for this object (row).
+     * @return int
+     */
+    public function getPrimaryKey()
+    {
+        return $this->getDbId();
+    }
+
+    /**
+     * Generic method to set the primary key (id column).
+     *
+     * @param  int $key Primary key.
+     * @return void
+     */
+    public function setPrimaryKey($key)
+    {
+        $this->setDbId($key);
+    }
+
+    /**
+     * Returns true if the primary key for this object is null.
+     * @return boolean
+     */
+    public function isPrimaryKeyNull()
+    {
+
+        return null === $this->getDbId();
+    }
+
+    /**
+     * Sets contents of passed object to values from current object.
+     *
+     * If desired, this method can also make copies of all associated (fkey referrers)
+     * objects.
+     *
+     * @param object $copyObj An object of ThirdPartyTrackReferences (or compatible) type.
+     * @param boolean $deepCopy Whether to also copy all rows that refer (by fkey) to the current row.
+     * @param boolean $makeNew Whether to reset autoincrement PKs and make the object new.
+     * @throws PropelException
+     */
+    public function copyInto($copyObj, $deepCopy = false, $makeNew = true)
+    {
+        $copyObj->setDbService($this->getDbService());
+        $copyObj->setDbForeignId($this->getDbForeignId());
+        $copyObj->setDbFileId($this->getDbFileId());
+        $copyObj->setDbStatus($this->getDbStatus());
+
+        if ($deepCopy && !$this->startCopy) {
+            // important: temporarily setNew(false) because this affects the behavior of
+            // the getter/setter methods for fkey referrer objects.
+            $copyObj->setNew(false);
+            // store object hash to prevent cycle
+            $this->startCopy = true;
+
+            //unflag object copy
+            $this->startCopy = false;
+        } // if ($deepCopy)
+
+        if ($makeNew) {
+            $copyObj->setNew(true);
+            $copyObj->setDbId(NULL); // this is a auto-increment column, so set to default value
+        }
+    }
+
+    /**
+     * Makes a copy of this object that will be inserted as a new row in table when saved.
+     * It creates a new object filling in the simple attributes, but skipping any primary
+     * keys that are defined for the table.
+     *
+     * If desired, this method can also make copies of all associated (fkey referrers)
+     * objects.
+     *
+     * @param boolean $deepCopy Whether to also copy all rows that refer (by fkey) to the current row.
+     * @return ThirdPartyTrackReferences Clone of current object.
+     * @throws PropelException
+     */
+    public function copy($deepCopy = false)
+    {
+        // we use get_class(), because this might be a subclass
+        $clazz = get_class($this);
+        $copyObj = new $clazz();
+        $this->copyInto($copyObj, $deepCopy);
+
+        return $copyObj;
+    }
+
+    /**
+     * Returns a peer instance associated with this om.
+     *
+     * Since Peer classes are not to have any instance attributes, this method returns the
+     * same instance for all member of this class. The method could therefore
+     * be static, but this would prevent one from overriding the behavior.
+     *
+     * @return ThirdPartyTrackReferencesPeer
+     */
+    public function getPeer()
+    {
+        if (self::$peer === null) {
+            self::$peer = new ThirdPartyTrackReferencesPeer();
+        }
+
+        return self::$peer;
+    }
+
+    /**
+     * Declares an association between this object and a CcPlayoutHistoryTemplate object.
+     *
+     * @param                  CcPlayoutHistoryTemplate $v
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     * @throws PropelException
+     */
+    public function setCcPlayoutHistoryTemplate(CcPlayoutHistoryTemplate $v = null)
+    {
+        if ($v === null) {
+            $this->setDbFileId(NULL);
+        } else {
+            $this->setDbFileId($v->getDbId());
+        }
+
+        $this->aCcPlayoutHistoryTemplate = $v;
+
+        // Add binding for other direction of this n:n relationship.
+        // If this object has already been added to the CcPlayoutHistoryTemplate object, it will not be re-added.
+        if ($v !== null) {
+            $v->addThirdPartyTrackReferences($this);
+        }
+
+
+        return $this;
+    }
+
+
+    /**
+     * Get the associated CcPlayoutHistoryTemplate object
+     *
+     * @param PropelPDO $con Optional Connection object.
+     * @param $doQuery Executes a query to get the object if required
+     * @return CcPlayoutHistoryTemplate The associated CcPlayoutHistoryTemplate object.
+     * @throws PropelException
+     */
+    public function getCcPlayoutHistoryTemplate(PropelPDO $con = null, $doQuery = true)
+    {
+        if ($this->aCcPlayoutHistoryTemplate === null && ($this->file_id !== null) && $doQuery) {
+            $this->aCcPlayoutHistoryTemplate = CcPlayoutHistoryTemplateQuery::create()->findPk($this->file_id, $con);
+            /* The following can be used additionally to
+                guarantee the related object contains a reference
+                to this object.  This level of coupling may, however, be
+                undesirable since it could result in an only partially populated collection
+                in the referenced object.
+                $this->aCcPlayoutHistoryTemplate->addThirdPartyTrackReferencess($this);
+             */
+        }
+
+        return $this->aCcPlayoutHistoryTemplate;
+    }
+
+    /**
+     * Clears the current object and sets all attributes to their default values
+     */
+    public function clear()
+    {
+        $this->id = null;
+        $this->service = null;
+        $this->foreign_id = null;
+        $this->file_id = null;
+        $this->status = null;
+        $this->alreadyInSave = false;
+        $this->alreadyInValidation = false;
+        $this->alreadyInClearAllReferencesDeep = false;
+        $this->clearAllReferences();
+        $this->resetModified();
+        $this->setNew(true);
+        $this->setDeleted(false);
+    }
+
+    /**
+     * Resets all references to other model objects or collections of model objects.
+     *
+     * This method is a user-space workaround for PHP's inability to garbage collect
+     * objects with circular references (even in PHP 5.3). This is currently necessary
+     * when using Propel in certain daemon or large-volume/high-memory operations.
+     *
+     * @param boolean $deep Whether to also clear the references on all referrer objects.
+     */
+    public function clearAllReferences($deep = false)
+    {
+        if ($deep && !$this->alreadyInClearAllReferencesDeep) {
+            $this->alreadyInClearAllReferencesDeep = true;
+            if ($this->aCcPlayoutHistoryTemplate instanceof Persistent) {
+              $this->aCcPlayoutHistoryTemplate->clearAllReferences($deep);
+            }
+
+            $this->alreadyInClearAllReferencesDeep = false;
+        } // if ($deep)
+
+        $this->aCcPlayoutHistoryTemplate = null;
+    }
+
+    /**
+     * return the string representation of this object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->exportTo(ThirdPartyTrackReferencesPeer::DEFAULT_STRING_FORMAT);
+    }
+
+    /**
+     * return true is the object is in saving state
+     *
+     * @return boolean
+     */
+    public function isAlreadyInSave()
+    {
+        return $this->alreadyInSave;
+    }
+
+}
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
new file mode 100644
index 000000000..20e769677
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
@@ -0,0 +1,1014 @@
+<?php
+
+
+/**
+ * Base static class for performing query and update operations on the 'third_party_track_references' table.
+ *
+ *
+ *
+ * @package propel.generator.airtime.om
+ */
+abstract class BaseThirdPartyTrackReferencesPeer
+{
+
+    /** the default database name for this class */
+    const DATABASE_NAME = 'airtime';
+
+    /** the table name for this class */
+    const TABLE_NAME = 'third_party_track_references';
+
+    /** the related Propel class for this table */
+    const OM_CLASS = 'ThirdPartyTrackReferences';
+
+    /** the related TableMap class for this table */
+    const TM_CLASS = 'ThirdPartyTrackReferencesTableMap';
+
+    /** The total number of columns. */
+    const NUM_COLUMNS = 5;
+
+    /** The number of lazy-loaded columns. */
+    const NUM_LAZY_LOAD_COLUMNS = 0;
+
+    /** The number of columns to hydrate (NUM_COLUMNS - NUM_LAZY_LOAD_COLUMNS) */
+    const NUM_HYDRATE_COLUMNS = 5;
+
+    /** the column name for the id field */
+    const ID = 'third_party_track_references.id';
+
+    /** the column name for the service field */
+    const SERVICE = 'third_party_track_references.service';
+
+    /** the column name for the foreign_id field */
+    const FOREIGN_ID = 'third_party_track_references.foreign_id';
+
+    /** the column name for the file_id field */
+    const FILE_ID = 'third_party_track_references.file_id';
+
+    /** the column name for the status field */
+    const STATUS = 'third_party_track_references.status';
+
+    /** The default string format for model objects of the related table **/
+    const DEFAULT_STRING_FORMAT = 'YAML';
+
+    /**
+     * An identity map to hold any loaded instances of ThirdPartyTrackReferences objects.
+     * This must be public so that other peer classes can access this when hydrating from JOIN
+     * queries.
+     * @var        array ThirdPartyTrackReferences[]
+     */
+    public static $instances = array();
+
+
+    /**
+     * holds an array of fieldnames
+     *
+     * first dimension keys are the type constants
+     * e.g. ThirdPartyTrackReferencesPeer::$fieldNames[ThirdPartyTrackReferencesPeer::TYPE_PHPNAME][0] = 'Id'
+     */
+    protected static $fieldNames = array (
+        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbService', 'DbForeignId', 'DbFileId', 'DbStatus', ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbService', 'dbForeignId', 'dbFileId', 'dbStatus', ),
+        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID, ThirdPartyTrackReferencesPeer::SERVICE, ThirdPartyTrackReferencesPeer::FOREIGN_ID, ThirdPartyTrackReferencesPeer::FILE_ID, ThirdPartyTrackReferencesPeer::STATUS, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'SERVICE', 'FOREIGN_ID', 'FILE_ID', 'STATUS', ),
+        BasePeer::TYPE_FIELDNAME => array ('id', 'service', 'foreign_id', 'file_id', 'status', ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+    );
+
+    /**
+     * holds an array of keys for quick access to the fieldnames array
+     *
+     * first dimension keys are the type constants
+     * e.g. ThirdPartyTrackReferencesPeer::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0
+     */
+    protected static $fieldKeys = array (
+        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbService' => 1, 'DbForeignId' => 2, 'DbFileId' => 3, 'DbStatus' => 4, ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbService' => 1, 'dbForeignId' => 2, 'dbFileId' => 3, 'dbStatus' => 4, ),
+        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID => 0, ThirdPartyTrackReferencesPeer::SERVICE => 1, ThirdPartyTrackReferencesPeer::FOREIGN_ID => 2, ThirdPartyTrackReferencesPeer::FILE_ID => 3, ThirdPartyTrackReferencesPeer::STATUS => 4, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'SERVICE' => 1, 'FOREIGN_ID' => 2, 'FILE_ID' => 3, 'STATUS' => 4, ),
+        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'service' => 1, 'foreign_id' => 2, 'file_id' => 3, 'status' => 4, ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+    );
+
+    /**
+     * Translates a fieldname to another type
+     *
+     * @param      string $name field name
+     * @param      string $fromType One of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *                         BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM
+     * @param      string $toType   One of the class type constants
+     * @return string          translated name of the field.
+     * @throws PropelException - if the specified name could not be found in the fieldname mappings.
+     */
+    public static function translateFieldName($name, $fromType, $toType)
+    {
+        $toNames = ThirdPartyTrackReferencesPeer::getFieldNames($toType);
+        $key = isset(ThirdPartyTrackReferencesPeer::$fieldKeys[$fromType][$name]) ? ThirdPartyTrackReferencesPeer::$fieldKeys[$fromType][$name] : null;
+        if ($key === null) {
+            throw new PropelException("'$name' could not be found in the field names of type '$fromType'. These are: " . print_r(ThirdPartyTrackReferencesPeer::$fieldKeys[$fromType], true));
+        }
+
+        return $toNames[$key];
+    }
+
+    /**
+     * Returns an array of field names.
+     *
+     * @param      string $type The type of fieldnames to return:
+     *                      One of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *                      BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM
+     * @return array           A list of field names
+     * @throws PropelException - if the type is not valid.
+     */
+    public static function getFieldNames($type = BasePeer::TYPE_PHPNAME)
+    {
+        if (!array_key_exists($type, ThirdPartyTrackReferencesPeer::$fieldNames)) {
+            throw new PropelException('Method getFieldNames() expects the parameter $type to be one of the class constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME, BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM. ' . $type . ' was given.');
+        }
+
+        return ThirdPartyTrackReferencesPeer::$fieldNames[$type];
+    }
+
+    /**
+     * Convenience method which changes table.column to alias.column.
+     *
+     * Using this method you can maintain SQL abstraction while using column aliases.
+     * <code>
+     *		$c->addAlias("alias1", TablePeer::TABLE_NAME);
+     *		$c->addJoin(TablePeer::alias("alias1", TablePeer::PRIMARY_KEY_COLUMN), TablePeer::PRIMARY_KEY_COLUMN);
+     * </code>
+     * @param      string $alias The alias for the current table.
+     * @param      string $column The column name for current table. (i.e. ThirdPartyTrackReferencesPeer::COLUMN_NAME).
+     * @return string
+     */
+    public static function alias($alias, $column)
+    {
+        return str_replace(ThirdPartyTrackReferencesPeer::TABLE_NAME.'.', $alias.'.', $column);
+    }
+
+    /**
+     * Add all the columns needed to create a new object.
+     *
+     * Note: any columns that were marked with lazyLoad="true" in the
+     * XML schema will not be added to the select list and only loaded
+     * on demand.
+     *
+     * @param      Criteria $criteria object containing the columns to add.
+     * @param      string   $alias    optional table alias
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function addSelectColumns(Criteria $criteria, $alias = null)
+    {
+        if (null === $alias) {
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::ID);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::SERVICE);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::FOREIGN_ID);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::FILE_ID);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::STATUS);
+        } else {
+            $criteria->addSelectColumn($alias . '.id');
+            $criteria->addSelectColumn($alias . '.service');
+            $criteria->addSelectColumn($alias . '.foreign_id');
+            $criteria->addSelectColumn($alias . '.file_id');
+            $criteria->addSelectColumn($alias . '.status');
+        }
+    }
+
+    /**
+     * Returns the number of rows matching criteria.
+     *
+     * @param      Criteria $criteria
+     * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
+     * @param      PropelPDO $con
+     * @return int Number of matching rows.
+     */
+    public static function doCount(Criteria $criteria, $distinct = false, PropelPDO $con = null)
+    {
+        // we may modify criteria, so copy it first
+        $criteria = clone $criteria;
+
+        // We need to set the primary table name, since in the case that there are no WHERE columns
+        // it will be impossible for the BasePeer::createSelectSql() method to determine which
+        // tables go into the FROM clause.
+        $criteria->setPrimaryTableName(ThirdPartyTrackReferencesPeer::TABLE_NAME);
+
+        if ($distinct && !in_array(Criteria::DISTINCT, $criteria->getSelectModifiers())) {
+            $criteria->setDistinct();
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        }
+
+        $criteria->clearOrderByColumns(); // ORDER BY won't ever affect the count
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME); // Set the correct dbName
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+        // BasePeer returns a PDOStatement
+        $stmt = BasePeer::doCount($criteria, $con);
+
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $count = (int) $row[0];
+        } else {
+            $count = 0; // no rows returned; we infer that means 0 matches.
+        }
+        $stmt->closeCursor();
+
+        return $count;
+    }
+    /**
+     * Selects one object from the DB.
+     *
+     * @param      Criteria $criteria object used to create the SELECT statement.
+     * @param      PropelPDO $con
+     * @return ThirdPartyTrackReferences
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelectOne(Criteria $criteria, PropelPDO $con = null)
+    {
+        $critcopy = clone $criteria;
+        $critcopy->setLimit(1);
+        $objects = ThirdPartyTrackReferencesPeer::doSelect($critcopy, $con);
+        if ($objects) {
+            return $objects[0];
+        }
+
+        return null;
+    }
+    /**
+     * Selects several row from the DB.
+     *
+     * @param      Criteria $criteria The Criteria object used to build the SELECT statement.
+     * @param      PropelPDO $con
+     * @return array           Array of selected Objects
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelect(Criteria $criteria, PropelPDO $con = null)
+    {
+        return ThirdPartyTrackReferencesPeer::populateObjects(ThirdPartyTrackReferencesPeer::doSelectStmt($criteria, $con));
+    }
+    /**
+     * Prepares the Criteria object and uses the parent doSelect() method to execute a PDOStatement.
+     *
+     * Use this method directly if you want to work with an executed statement directly (for example
+     * to perform your own object hydration).
+     *
+     * @param      Criteria $criteria The Criteria object used to build the SELECT statement.
+     * @param      PropelPDO $con The connection to use
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     * @return PDOStatement The executed PDOStatement object.
+     * @see        BasePeer::doSelect()
+     */
+    public static function doSelectStmt(Criteria $criteria, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            $criteria = clone $criteria;
+            ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        }
+
+        // Set the correct dbName
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        // BasePeer returns a PDOStatement
+        return BasePeer::doSelect($criteria, $con);
+    }
+    /**
+     * Adds an object to the instance pool.
+     *
+     * Propel keeps cached copies of objects in an instance pool when they are retrieved
+     * from the database.  In some cases -- especially when you override doSelect*()
+     * methods in your stub classes -- you may need to explicitly add objects
+     * to the cache in order to ensure that the same objects are always returned by doSelect*()
+     * and retrieveByPK*() calls.
+     *
+     * @param ThirdPartyTrackReferences $obj A ThirdPartyTrackReferences object.
+     * @param      string $key (optional) key to use for instance map (for performance boost if key was already calculated externally).
+     */
+    public static function addInstanceToPool($obj, $key = null)
+    {
+        if (Propel::isInstancePoolingEnabled()) {
+            if ($key === null) {
+                $key = (string) $obj->getDbId();
+            } // if key === null
+            ThirdPartyTrackReferencesPeer::$instances[$key] = $obj;
+        }
+    }
+
+    /**
+     * Removes an object from the instance pool.
+     *
+     * Propel keeps cached copies of objects in an instance pool when they are retrieved
+     * from the database.  In some cases -- especially when you override doDelete
+     * methods in your stub classes -- you may need to explicitly remove objects
+     * from the cache in order to prevent returning objects that no longer exist.
+     *
+     * @param      mixed $value A ThirdPartyTrackReferences object or a primary key value.
+     *
+     * @return void
+     * @throws PropelException - if the value is invalid.
+     */
+    public static function removeInstanceFromPool($value)
+    {
+        if (Propel::isInstancePoolingEnabled() && $value !== null) {
+            if (is_object($value) && $value instanceof ThirdPartyTrackReferences) {
+                $key = (string) $value->getDbId();
+            } elseif (is_scalar($value)) {
+                // assume we've been passed a primary key
+                $key = (string) $value;
+            } else {
+                $e = new PropelException("Invalid value passed to removeInstanceFromPool().  Expected primary key or ThirdPartyTrackReferences object; got " . (is_object($value) ? get_class($value) . ' object.' : var_export($value,true)));
+                throw $e;
+            }
+
+            unset(ThirdPartyTrackReferencesPeer::$instances[$key]);
+        }
+    } // removeInstanceFromPool()
+
+    /**
+     * Retrieves a string version of the primary key from the DB resultset row that can be used to uniquely identify a row in this table.
+     *
+     * For tables with a single-column primary key, that simple pkey value will be returned.  For tables with
+     * a multi-column primary key, a serialize()d version of the primary key will be returned.
+     *
+     * @param      string $key The key (@see getPrimaryKeyHash()) for this instance.
+     * @return ThirdPartyTrackReferences Found object or null if 1) no instance exists for specified key or 2) instance pooling has been disabled.
+     * @see        getPrimaryKeyHash()
+     */
+    public static function getInstanceFromPool($key)
+    {
+        if (Propel::isInstancePoolingEnabled()) {
+            if (isset(ThirdPartyTrackReferencesPeer::$instances[$key])) {
+                return ThirdPartyTrackReferencesPeer::$instances[$key];
+            }
+        }
+
+        return null; // just to be explicit
+    }
+
+    /**
+     * Clear the instance pool.
+     *
+     * @return void
+     */
+    public static function clearInstancePool($and_clear_all_references = false)
+    {
+      if ($and_clear_all_references) {
+        foreach (ThirdPartyTrackReferencesPeer::$instances as $instance) {
+          $instance->clearAllReferences(true);
+        }
+      }
+        ThirdPartyTrackReferencesPeer::$instances = array();
+    }
+
+    /**
+     * Method to invalidate the instance pool of all tables related to third_party_track_references
+     * by a foreign key with ON DELETE CASCADE
+     */
+    public static function clearRelatedInstancePool()
+    {
+    }
+
+    /**
+     * Retrieves a string version of the primary key from the DB resultset row that can be used to uniquely identify a row in this table.
+     *
+     * For tables with a single-column primary key, that simple pkey value will be returned.  For tables with
+     * a multi-column primary key, a serialize()d version of the primary key will be returned.
+     *
+     * @param      array $row PropelPDO resultset row.
+     * @param      int $startcol The 0-based offset for reading from the resultset row.
+     * @return string A string version of PK or null if the components of primary key in result array are all null.
+     */
+    public static function getPrimaryKeyHashFromRow($row, $startcol = 0)
+    {
+        // If the PK cannot be derived from the row, return null.
+        if ($row[$startcol] === null) {
+            return null;
+        }
+
+        return (string) $row[$startcol];
+    }
+
+    /**
+     * Retrieves the primary key from the DB resultset row
+     * For tables with a single-column primary key, that simple pkey value will be returned.  For tables with
+     * a multi-column primary key, an array of the primary key columns will be returned.
+     *
+     * @param      array $row PropelPDO resultset row.
+     * @param      int $startcol The 0-based offset for reading from the resultset row.
+     * @return mixed The primary key of the row
+     */
+    public static function getPrimaryKeyFromRow($row, $startcol = 0)
+    {
+
+        return (int) $row[$startcol];
+    }
+
+    /**
+     * The returned array will contain objects of the default type or
+     * objects that inherit from the default.
+     *
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function populateObjects(PDOStatement $stmt)
+    {
+        $results = array();
+
+        // set the class once to avoid overhead in the loop
+        $cls = ThirdPartyTrackReferencesPeer::getOMClass();
+        // populate the object(s)
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $key = ThirdPartyTrackReferencesPeer::getPrimaryKeyHashFromRow($row, 0);
+            if (null !== ($obj = ThirdPartyTrackReferencesPeer::getInstanceFromPool($key))) {
+                // We no longer rehydrate the object, since this can cause data loss.
+                // See http://www.propelorm.org/ticket/509
+                // $obj->hydrate($row, 0, true); // rehydrate
+                $results[] = $obj;
+            } else {
+                $obj = new $cls();
+                $obj->hydrate($row);
+                $results[] = $obj;
+                ThirdPartyTrackReferencesPeer::addInstanceToPool($obj, $key);
+            } // if key exists
+        }
+        $stmt->closeCursor();
+
+        return $results;
+    }
+    /**
+     * Populates an object of the default type or an object that inherit from the default.
+     *
+     * @param      array $row PropelPDO resultset row.
+     * @param      int $startcol The 0-based offset for reading from the resultset row.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     * @return array (ThirdPartyTrackReferences object, last column rank)
+     */
+    public static function populateObject($row, $startcol = 0)
+    {
+        $key = ThirdPartyTrackReferencesPeer::getPrimaryKeyHashFromRow($row, $startcol);
+        if (null !== ($obj = ThirdPartyTrackReferencesPeer::getInstanceFromPool($key))) {
+            // We no longer rehydrate the object, since this can cause data loss.
+            // See http://www.propelorm.org/ticket/509
+            // $obj->hydrate($row, $startcol, true); // rehydrate
+            $col = $startcol + ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS;
+        } else {
+            $cls = ThirdPartyTrackReferencesPeer::OM_CLASS;
+            $obj = new $cls();
+            $col = $obj->hydrate($row, $startcol);
+            ThirdPartyTrackReferencesPeer::addInstanceToPool($obj, $key);
+        }
+
+        return array($obj, $col);
+    }
+
+
+    /**
+     * Returns the number of rows matching criteria, joining the related CcPlayoutHistoryTemplate table
+     *
+     * @param      Criteria $criteria
+     * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return int Number of matching rows.
+     */
+    public static function doCountJoinCcPlayoutHistoryTemplate(Criteria $criteria, $distinct = false, PropelPDO $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        // we're going to modify criteria, so copy it first
+        $criteria = clone $criteria;
+
+        // We need to set the primary table name, since in the case that there are no WHERE columns
+        // it will be impossible for the BasePeer::createSelectSql() method to determine which
+        // tables go into the FROM clause.
+        $criteria->setPrimaryTableName(ThirdPartyTrackReferencesPeer::TABLE_NAME);
+
+        if ($distinct && !in_array(Criteria::DISTINCT, $criteria->getSelectModifiers())) {
+            $criteria->setDistinct();
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        }
+
+        $criteria->clearOrderByColumns(); // ORDER BY won't ever affect the count
+
+        // Set the correct dbName
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doCount($criteria, $con);
+
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $count = (int) $row[0];
+        } else {
+            $count = 0; // no rows returned; we infer that means 0 matches.
+        }
+        $stmt->closeCursor();
+
+        return $count;
+    }
+
+
+    /**
+     * Selects a collection of ThirdPartyTrackReferences objects pre-filled with their CcPlayoutHistoryTemplate objects.
+     * @param      Criteria  $criteria
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return array           Array of ThirdPartyTrackReferences objects.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelectJoinCcPlayoutHistoryTemplate(Criteria $criteria, $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        $criteria = clone $criteria;
+
+        // Set the correct dbName if it has not been overridden
+        if ($criteria->getDbName() == Propel::getDefaultDB()) {
+            $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+        }
+
+        ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        $startcol = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS;
+        CcPlayoutHistoryTemplatePeer::addSelectColumns($criteria);
+
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doSelect($criteria, $con);
+        $results = array();
+
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $key1 = ThirdPartyTrackReferencesPeer::getPrimaryKeyHashFromRow($row, 0);
+            if (null !== ($obj1 = ThirdPartyTrackReferencesPeer::getInstanceFromPool($key1))) {
+                // We no longer rehydrate the object, since this can cause data loss.
+                // See http://www.propelorm.org/ticket/509
+                // $obj1->hydrate($row, 0, true); // rehydrate
+            } else {
+
+                $cls = ThirdPartyTrackReferencesPeer::getOMClass();
+
+                $obj1 = new $cls();
+                $obj1->hydrate($row);
+                ThirdPartyTrackReferencesPeer::addInstanceToPool($obj1, $key1);
+            } // if $obj1 already loaded
+
+            $key2 = CcPlayoutHistoryTemplatePeer::getPrimaryKeyHashFromRow($row, $startcol);
+            if ($key2 !== null) {
+                $obj2 = CcPlayoutHistoryTemplatePeer::getInstanceFromPool($key2);
+                if (!$obj2) {
+
+                    $cls = CcPlayoutHistoryTemplatePeer::getOMClass();
+
+                    $obj2 = new $cls();
+                    $obj2->hydrate($row, $startcol);
+                    CcPlayoutHistoryTemplatePeer::addInstanceToPool($obj2, $key2);
+                } // if obj2 already loaded
+
+                // Add the $obj1 (ThirdPartyTrackReferences) to $obj2 (CcPlayoutHistoryTemplate)
+                $obj2->addThirdPartyTrackReferences($obj1);
+
+            } // if joined row was not null
+
+            $results[] = $obj1;
+        }
+        $stmt->closeCursor();
+
+        return $results;
+    }
+
+
+    /**
+     * Returns the number of rows matching criteria, joining all related tables
+     *
+     * @param      Criteria $criteria
+     * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return int Number of matching rows.
+     */
+    public static function doCountJoinAll(Criteria $criteria, $distinct = false, PropelPDO $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        // we're going to modify criteria, so copy it first
+        $criteria = clone $criteria;
+
+        // We need to set the primary table name, since in the case that there are no WHERE columns
+        // it will be impossible for the BasePeer::createSelectSql() method to determine which
+        // tables go into the FROM clause.
+        $criteria->setPrimaryTableName(ThirdPartyTrackReferencesPeer::TABLE_NAME);
+
+        if ($distinct && !in_array(Criteria::DISTINCT, $criteria->getSelectModifiers())) {
+            $criteria->setDistinct();
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        }
+
+        $criteria->clearOrderByColumns(); // ORDER BY won't ever affect the count
+
+        // Set the correct dbName
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doCount($criteria, $con);
+
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $count = (int) $row[0];
+        } else {
+            $count = 0; // no rows returned; we infer that means 0 matches.
+        }
+        $stmt->closeCursor();
+
+        return $count;
+    }
+
+    /**
+     * Selects a collection of ThirdPartyTrackReferences objects pre-filled with all related objects.
+     *
+     * @param      Criteria  $criteria
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return array           Array of ThirdPartyTrackReferences objects.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelectJoinAll(Criteria $criteria, $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        $criteria = clone $criteria;
+
+        // Set the correct dbName if it has not been overridden
+        if ($criteria->getDbName() == Propel::getDefaultDB()) {
+            $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+        }
+
+        ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        $startcol2 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS;
+
+        CcPlayoutHistoryTemplatePeer::addSelectColumns($criteria);
+        $startcol3 = $startcol2 + CcPlayoutHistoryTemplatePeer::NUM_HYDRATE_COLUMNS;
+
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doSelect($criteria, $con);
+        $results = array();
+
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $key1 = ThirdPartyTrackReferencesPeer::getPrimaryKeyHashFromRow($row, 0);
+            if (null !== ($obj1 = ThirdPartyTrackReferencesPeer::getInstanceFromPool($key1))) {
+                // We no longer rehydrate the object, since this can cause data loss.
+                // See http://www.propelorm.org/ticket/509
+                // $obj1->hydrate($row, 0, true); // rehydrate
+            } else {
+                $cls = ThirdPartyTrackReferencesPeer::getOMClass();
+
+                $obj1 = new $cls();
+                $obj1->hydrate($row);
+                ThirdPartyTrackReferencesPeer::addInstanceToPool($obj1, $key1);
+            } // if obj1 already loaded
+
+            // Add objects for joined CcPlayoutHistoryTemplate rows
+
+            $key2 = CcPlayoutHistoryTemplatePeer::getPrimaryKeyHashFromRow($row, $startcol2);
+            if ($key2 !== null) {
+                $obj2 = CcPlayoutHistoryTemplatePeer::getInstanceFromPool($key2);
+                if (!$obj2) {
+
+                    $cls = CcPlayoutHistoryTemplatePeer::getOMClass();
+
+                    $obj2 = new $cls();
+                    $obj2->hydrate($row, $startcol2);
+                    CcPlayoutHistoryTemplatePeer::addInstanceToPool($obj2, $key2);
+                } // if obj2 loaded
+
+                // Add the $obj1 (ThirdPartyTrackReferences) to the collection in $obj2 (CcPlayoutHistoryTemplate)
+                $obj2->addThirdPartyTrackReferences($obj1);
+            } // if joined row not null
+
+            $results[] = $obj1;
+        }
+        $stmt->closeCursor();
+
+        return $results;
+    }
+
+    /**
+     * Returns the TableMap related to this peer.
+     * This method is not needed for general use but a specific application could have a need.
+     * @return TableMap
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function getTableMap()
+    {
+        return Propel::getDatabaseMap(ThirdPartyTrackReferencesPeer::DATABASE_NAME)->getTable(ThirdPartyTrackReferencesPeer::TABLE_NAME);
+    }
+
+    /**
+     * Add a TableMap instance to the database for this peer class.
+     */
+    public static function buildTableMap()
+    {
+      $dbMap = Propel::getDatabaseMap(BaseThirdPartyTrackReferencesPeer::DATABASE_NAME);
+      if (!$dbMap->hasTable(BaseThirdPartyTrackReferencesPeer::TABLE_NAME)) {
+        $dbMap->addTableObject(new \ThirdPartyTrackReferencesTableMap());
+      }
+    }
+
+    /**
+     * The class that the Peer will make instances of.
+     *
+     *
+     * @return string ClassName
+     */
+    public static function getOMClass($row = 0, $colnum = 0)
+    {
+        return ThirdPartyTrackReferencesPeer::OM_CLASS;
+    }
+
+    /**
+     * Performs an INSERT on the database, given a ThirdPartyTrackReferences or Criteria object.
+     *
+     * @param      mixed $values Criteria or ThirdPartyTrackReferences object containing data that is used to create the INSERT statement.
+     * @param      PropelPDO $con the PropelPDO connection to use
+     * @return mixed           The new primary key.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doInsert($values, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        if ($values instanceof Criteria) {
+            $criteria = clone $values; // rename for clarity
+        } else {
+            $criteria = $values->buildCriteria(); // build Criteria from ThirdPartyTrackReferences object
+        }
+
+        if ($criteria->containsKey(ThirdPartyTrackReferencesPeer::ID) && $criteria->keyContainsValue(ThirdPartyTrackReferencesPeer::ID) ) {
+            throw new PropelException('Cannot insert a value for auto-increment primary key ('.ThirdPartyTrackReferencesPeer::ID.')');
+        }
+
+
+        // Set the correct dbName
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        try {
+            // use transaction because $criteria could contain info
+            // for more than one table (I guess, conceivably)
+            $con->beginTransaction();
+            $pk = BasePeer::doInsert($criteria, $con);
+            $con->commit();
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+
+        return $pk;
+    }
+
+    /**
+     * Performs an UPDATE on the database, given a ThirdPartyTrackReferences or Criteria object.
+     *
+     * @param      mixed $values Criteria or ThirdPartyTrackReferences object containing data that is used to create the UPDATE statement.
+     * @param      PropelPDO $con The connection to use (specify PropelPDO connection object to exert more control over transactions).
+     * @return int             The number of affected rows (if supported by underlying database driver).
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doUpdate($values, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        $selectCriteria = new Criteria(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        if ($values instanceof Criteria) {
+            $criteria = clone $values; // rename for clarity
+
+            $comparison = $criteria->getComparison(ThirdPartyTrackReferencesPeer::ID);
+            $value = $criteria->remove(ThirdPartyTrackReferencesPeer::ID);
+            if ($value) {
+                $selectCriteria->add(ThirdPartyTrackReferencesPeer::ID, $value, $comparison);
+            } else {
+                $selectCriteria->setPrimaryTableName(ThirdPartyTrackReferencesPeer::TABLE_NAME);
+            }
+
+        } else { // $values is ThirdPartyTrackReferences object
+            $criteria = $values->buildCriteria(); // gets full criteria
+            $selectCriteria = $values->buildPkeyCriteria(); // gets criteria w/ primary key(s)
+        }
+
+        // set the correct dbName
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        return BasePeer::doUpdate($selectCriteria, $criteria, $con);
+    }
+
+    /**
+     * Deletes all rows from the third_party_track_references table.
+     *
+     * @param      PropelPDO $con the connection to use
+     * @return int             The number of affected rows (if supported by underlying database driver).
+     * @throws PropelException
+     */
+    public static function doDeleteAll(PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+        $affectedRows = 0; // initialize var to track total num of affected rows
+        try {
+            // use transaction because $criteria could contain info
+            // for more than one table or we could emulating ON DELETE CASCADE, etc.
+            $con->beginTransaction();
+            $affectedRows += BasePeer::doDeleteAll(ThirdPartyTrackReferencesPeer::TABLE_NAME, $con, ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+            // Because this db requires some delete cascade/set null emulation, we have to
+            // clear the cached instance *after* the emulation has happened (since
+            // instances get re-added by the select statement contained therein).
+            ThirdPartyTrackReferencesPeer::clearInstancePool();
+            ThirdPartyTrackReferencesPeer::clearRelatedInstancePool();
+            $con->commit();
+
+            return $affectedRows;
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Performs a DELETE on the database, given a ThirdPartyTrackReferences or Criteria object OR a primary key value.
+     *
+     * @param      mixed $values Criteria or ThirdPartyTrackReferences object or primary key or array of primary keys
+     *              which is used to create the DELETE statement
+     * @param      PropelPDO $con the connection to use
+     * @return int The number of affected rows (if supported by underlying database driver).  This includes CASCADE-related rows
+     *				if supported by native driver or if emulated using Propel.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+     public static function doDelete($values, PropelPDO $con = null)
+     {
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        if ($values instanceof Criteria) {
+            // invalidate the cache for all objects of this type, since we have no
+            // way of knowing (without running a query) what objects should be invalidated
+            // from the cache based on this Criteria.
+            ThirdPartyTrackReferencesPeer::clearInstancePool();
+            // rename for clarity
+            $criteria = clone $values;
+        } elseif ($values instanceof ThirdPartyTrackReferences) { // it's a model object
+            // invalidate the cache for this single object
+            ThirdPartyTrackReferencesPeer::removeInstanceFromPool($values);
+            // create criteria based on pk values
+            $criteria = $values->buildPkeyCriteria();
+        } else { // it's a primary key, or an array of pks
+            $criteria = new Criteria(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+            $criteria->add(ThirdPartyTrackReferencesPeer::ID, (array) $values, Criteria::IN);
+            // invalidate the cache for this object(s)
+            foreach ((array) $values as $singleval) {
+                ThirdPartyTrackReferencesPeer::removeInstanceFromPool($singleval);
+            }
+        }
+
+        // Set the correct dbName
+        $criteria->setDbName(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+
+        $affectedRows = 0; // initialize var to track total num of affected rows
+
+        try {
+            // use transaction because $criteria could contain info
+            // for more than one table or we could emulating ON DELETE CASCADE, etc.
+            $con->beginTransaction();
+
+            $affectedRows += BasePeer::doDelete($criteria, $con);
+            ThirdPartyTrackReferencesPeer::clearRelatedInstancePool();
+            $con->commit();
+
+            return $affectedRows;
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Validates all modified columns of given ThirdPartyTrackReferences object.
+     * If parameter $columns is either a single column name or an array of column names
+     * than only those columns are validated.
+     *
+     * NOTICE: This does not apply to primary or foreign keys for now.
+     *
+     * @param ThirdPartyTrackReferences $obj The object to validate.
+     * @param      mixed $cols Column name or array of column names.
+     *
+     * @return mixed TRUE if all columns are valid or the error message of the first invalid column.
+     */
+    public static function doValidate($obj, $cols = null)
+    {
+        $columns = array();
+
+        if ($cols) {
+            $dbMap = Propel::getDatabaseMap(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+            $tableMap = $dbMap->getTable(ThirdPartyTrackReferencesPeer::TABLE_NAME);
+
+            if (! is_array($cols)) {
+                $cols = array($cols);
+            }
+
+            foreach ($cols as $colName) {
+                if ($tableMap->hasColumn($colName)) {
+                    $get = 'get' . $tableMap->getColumn($colName)->getPhpName();
+                    $columns[$colName] = $obj->$get();
+                }
+            }
+        } else {
+
+        }
+
+        return BasePeer::doValidate(ThirdPartyTrackReferencesPeer::DATABASE_NAME, ThirdPartyTrackReferencesPeer::TABLE_NAME, $columns);
+    }
+
+    /**
+     * Retrieve a single object by pkey.
+     *
+     * @param int $pk the primary key.
+     * @param      PropelPDO $con the connection to use
+     * @return ThirdPartyTrackReferences
+     */
+    public static function retrieveByPK($pk, PropelPDO $con = null)
+    {
+
+        if (null !== ($obj = ThirdPartyTrackReferencesPeer::getInstanceFromPool((string) $pk))) {
+            return $obj;
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $criteria = new Criteria(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+        $criteria->add(ThirdPartyTrackReferencesPeer::ID, $pk);
+
+        $v = ThirdPartyTrackReferencesPeer::doSelect($criteria, $con);
+
+        return !empty($v) > 0 ? $v[0] : null;
+    }
+
+    /**
+     * Retrieve multiple objects by pkey.
+     *
+     * @param      array $pks List of primary keys
+     * @param      PropelPDO $con the connection to use
+     * @return ThirdPartyTrackReferences[]
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function retrieveByPKs($pks, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $objs = null;
+        if (empty($pks)) {
+            $objs = array();
+        } else {
+            $criteria = new Criteria(ThirdPartyTrackReferencesPeer::DATABASE_NAME);
+            $criteria->add(ThirdPartyTrackReferencesPeer::ID, $pks, Criteria::IN);
+            $objs = ThirdPartyTrackReferencesPeer::doSelect($criteria, $con);
+        }
+
+        return $objs;
+    }
+
+} // BaseThirdPartyTrackReferencesPeer
+
+// This is the static code needed to register the TableMap for this table with the main Propel class.
+//
+BaseThirdPartyTrackReferencesPeer::buildTableMap();
+
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
new file mode 100644
index 000000000..29ac981eb
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
@@ -0,0 +1,516 @@
+<?php
+
+
+/**
+ * Base class that represents a query for the 'third_party_track_references' table.
+ *
+ *
+ *
+ * @method ThirdPartyTrackReferencesQuery orderByDbId($order = Criteria::ASC) Order by the id column
+ * @method ThirdPartyTrackReferencesQuery orderByDbService($order = Criteria::ASC) Order by the service column
+ * @method ThirdPartyTrackReferencesQuery orderByDbForeignId($order = Criteria::ASC) Order by the foreign_id column
+ * @method ThirdPartyTrackReferencesQuery orderByDbFileId($order = Criteria::ASC) Order by the file_id column
+ * @method ThirdPartyTrackReferencesQuery orderByDbStatus($order = Criteria::ASC) Order by the status column
+ *
+ * @method ThirdPartyTrackReferencesQuery groupByDbId() Group by the id column
+ * @method ThirdPartyTrackReferencesQuery groupByDbService() Group by the service column
+ * @method ThirdPartyTrackReferencesQuery groupByDbForeignId() Group by the foreign_id column
+ * @method ThirdPartyTrackReferencesQuery groupByDbFileId() Group by the file_id column
+ * @method ThirdPartyTrackReferencesQuery groupByDbStatus() Group by the status column
+ *
+ * @method ThirdPartyTrackReferencesQuery leftJoin($relation) Adds a LEFT JOIN clause to the query
+ * @method ThirdPartyTrackReferencesQuery rightJoin($relation) Adds a RIGHT JOIN clause to the query
+ * @method ThirdPartyTrackReferencesQuery innerJoin($relation) Adds a INNER JOIN clause to the query
+ *
+ * @method ThirdPartyTrackReferencesQuery leftJoinCcPlayoutHistoryTemplate($relationAlias = null) Adds a LEFT JOIN clause to the query using the CcPlayoutHistoryTemplate relation
+ * @method ThirdPartyTrackReferencesQuery rightJoinCcPlayoutHistoryTemplate($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CcPlayoutHistoryTemplate relation
+ * @method ThirdPartyTrackReferencesQuery innerJoinCcPlayoutHistoryTemplate($relationAlias = null) Adds a INNER JOIN clause to the query using the CcPlayoutHistoryTemplate relation
+ *
+ * @method ThirdPartyTrackReferences findOne(PropelPDO $con = null) Return the first ThirdPartyTrackReferences matching the query
+ * @method ThirdPartyTrackReferences findOneOrCreate(PropelPDO $con = null) Return the first ThirdPartyTrackReferences matching the query, or a new ThirdPartyTrackReferences object populated from the query conditions when no match is found
+ *
+ * @method ThirdPartyTrackReferences findOneByDbService(string $service) Return the first ThirdPartyTrackReferences filtered by the service column
+ * @method ThirdPartyTrackReferences findOneByDbForeignId(int $foreign_id) Return the first ThirdPartyTrackReferences filtered by the foreign_id column
+ * @method ThirdPartyTrackReferences findOneByDbFileId(int $file_id) Return the first ThirdPartyTrackReferences filtered by the file_id column
+ * @method ThirdPartyTrackReferences findOneByDbStatus(string $status) Return the first ThirdPartyTrackReferences filtered by the status column
+ *
+ * @method array findByDbId(int $id) Return ThirdPartyTrackReferences objects filtered by the id column
+ * @method array findByDbService(string $service) Return ThirdPartyTrackReferences objects filtered by the service column
+ * @method array findByDbForeignId(int $foreign_id) Return ThirdPartyTrackReferences objects filtered by the foreign_id column
+ * @method array findByDbFileId(int $file_id) Return ThirdPartyTrackReferences objects filtered by the file_id column
+ * @method array findByDbStatus(string $status) Return ThirdPartyTrackReferences objects filtered by the status column
+ *
+ * @package    propel.generator.airtime.om
+ */
+abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
+{
+    /**
+     * Initializes internal state of BaseThirdPartyTrackReferencesQuery object.
+     *
+     * @param     string $dbName The dabase name
+     * @param     string $modelName The phpName of a model, e.g. 'Book'
+     * @param     string $modelAlias The alias for the model in this query, e.g. 'b'
+     */
+    public function __construct($dbName = null, $modelName = null, $modelAlias = null)
+    {
+        if (null === $dbName) {
+            $dbName = 'airtime';
+        }
+        if (null === $modelName) {
+            $modelName = 'ThirdPartyTrackReferences';
+        }
+        parent::__construct($dbName, $modelName, $modelAlias);
+    }
+
+    /**
+     * Returns a new ThirdPartyTrackReferencesQuery object.
+     *
+     * @param     string $modelAlias The alias of a model in the query
+     * @param   ThirdPartyTrackReferencesQuery|Criteria $criteria Optional Criteria to build the query from
+     *
+     * @return ThirdPartyTrackReferencesQuery
+     */
+    public static function create($modelAlias = null, $criteria = null)
+    {
+        if ($criteria instanceof ThirdPartyTrackReferencesQuery) {
+            return $criteria;
+        }
+        $query = new ThirdPartyTrackReferencesQuery(null, null, $modelAlias);
+
+        if ($criteria instanceof Criteria) {
+            $query->mergeWith($criteria);
+        }
+
+        return $query;
+    }
+
+    /**
+     * Find object by primary key.
+     * Propel uses the instance pool to skip the database if the object exists.
+     * Go fast if the query is untouched.
+     *
+     * <code>
+     * $obj  = $c->findPk(12, $con);
+     * </code>
+     *
+     * @param mixed $key Primary key to use for the query
+     * @param     PropelPDO $con an optional connection object
+     *
+     * @return   ThirdPartyTrackReferences|ThirdPartyTrackReferences[]|mixed the result, formatted by the current formatter
+     */
+    public function findPk($key, $con = null)
+    {
+        if ($key === null) {
+            return null;
+        }
+        if ((null !== ($obj = ThirdPartyTrackReferencesPeer::getInstanceFromPool((string) $key))) && !$this->formatter) {
+            // the object is already in the instance pool
+            return $obj;
+        }
+        if ($con === null) {
+            $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+        $this->basePreSelect($con);
+        if ($this->formatter || $this->modelAlias || $this->with || $this->select
+         || $this->selectColumns || $this->asColumns || $this->selectModifiers
+         || $this->map || $this->having || $this->joins) {
+            return $this->findPkComplex($key, $con);
+        } else {
+            return $this->findPkSimple($key, $con);
+        }
+    }
+
+    /**
+     * Alias of findPk to use instance pooling
+     *
+     * @param     mixed $key Primary key to use for the query
+     * @param     PropelPDO $con A connection object
+     *
+     * @return                 ThirdPartyTrackReferences A model object, or null if the key is not found
+     * @throws PropelException
+     */
+     public function findOneByDbId($key, $con = null)
+     {
+        return $this->findPk($key, $con);
+     }
+
+    /**
+     * Find object by primary key using raw SQL to go fast.
+     * Bypass doSelect() and the object formatter by using generated code.
+     *
+     * @param     mixed $key Primary key to use for the query
+     * @param     PropelPDO $con A connection object
+     *
+     * @return                 ThirdPartyTrackReferences A model object, or null if the key is not found
+     * @throws PropelException
+     */
+    protected function findPkSimple($key, $con)
+    {
+        $sql = 'SELECT "id", "service", "foreign_id", "file_id", "status" FROM "third_party_track_references" WHERE "id" = :p0';
+        try {
+            $stmt = $con->prepare($sql);
+            $stmt->bindValue(':p0', $key, PDO::PARAM_INT);
+            $stmt->execute();
+        } catch (Exception $e) {
+            Propel::log($e->getMessage(), Propel::LOG_ERR);
+            throw new PropelException(sprintf('Unable to execute SELECT statement [%s]', $sql), $e);
+        }
+        $obj = null;
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $obj = new ThirdPartyTrackReferences();
+            $obj->hydrate($row);
+            ThirdPartyTrackReferencesPeer::addInstanceToPool($obj, (string) $key);
+        }
+        $stmt->closeCursor();
+
+        return $obj;
+    }
+
+    /**
+     * Find object by primary key.
+     *
+     * @param     mixed $key Primary key to use for the query
+     * @param     PropelPDO $con A connection object
+     *
+     * @return ThirdPartyTrackReferences|ThirdPartyTrackReferences[]|mixed the result, formatted by the current formatter
+     */
+    protected function findPkComplex($key, $con)
+    {
+        // As the query uses a PK condition, no limit(1) is necessary.
+        $criteria = $this->isKeepQuery() ? clone $this : $this;
+        $stmt = $criteria
+            ->filterByPrimaryKey($key)
+            ->doSelect($con);
+
+        return $criteria->getFormatter()->init($criteria)->formatOne($stmt);
+    }
+
+    /**
+     * Find objects by primary key
+     * <code>
+     * $objs = $c->findPks(array(12, 56, 832), $con);
+     * </code>
+     * @param     array $keys Primary keys to use for the query
+     * @param     PropelPDO $con an optional connection object
+     *
+     * @return PropelObjectCollection|ThirdPartyTrackReferences[]|mixed the list of results, formatted by the current formatter
+     */
+    public function findPks($keys, $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection($this->getDbName(), Propel::CONNECTION_READ);
+        }
+        $this->basePreSelect($con);
+        $criteria = $this->isKeepQuery() ? clone $this : $this;
+        $stmt = $criteria
+            ->filterByPrimaryKeys($keys)
+            ->doSelect($con);
+
+        return $criteria->getFormatter()->init($criteria)->format($stmt);
+    }
+
+    /**
+     * Filter the query by primary key
+     *
+     * @param     mixed $key Primary key to use for the query
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByPrimaryKey($key)
+    {
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $key, Criteria::EQUAL);
+    }
+
+    /**
+     * Filter the query by a list of primary keys
+     *
+     * @param     array $keys The list of primary key to use for the query
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByPrimaryKeys($keys)
+    {
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $keys, Criteria::IN);
+    }
+
+    /**
+     * Filter the query on the id column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbId(1234); // WHERE id = 1234
+     * $query->filterByDbId(array(12, 34)); // WHERE id IN (12, 34)
+     * $query->filterByDbId(array('min' => 12)); // WHERE id >= 12
+     * $query->filterByDbId(array('max' => 12)); // WHERE id <= 12
+     * </code>
+     *
+     * @param     mixed $dbId The value to use as filter.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbId($dbId = null, $comparison = null)
+    {
+        if (is_array($dbId)) {
+            $useMinMax = false;
+            if (isset($dbId['min'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $dbId['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbId['max'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $dbId['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $dbId, $comparison);
+    }
+
+    /**
+     * Filter the query on the service column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbService('fooValue');   // WHERE service = 'fooValue'
+     * $query->filterByDbService('%fooValue%'); // WHERE service LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbService The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbService($dbService = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbService)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbService)) {
+                $dbService = str_replace('*', '%', $dbService);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::SERVICE, $dbService, $comparison);
+    }
+
+    /**
+     * Filter the query on the foreign_id column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbForeignId(1234); // WHERE foreign_id = 1234
+     * $query->filterByDbForeignId(array(12, 34)); // WHERE foreign_id IN (12, 34)
+     * $query->filterByDbForeignId(array('min' => 12)); // WHERE foreign_id >= 12
+     * $query->filterByDbForeignId(array('max' => 12)); // WHERE foreign_id <= 12
+     * </code>
+     *
+     * @param     mixed $dbForeignId The value to use as filter.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbForeignId($dbForeignId = null, $comparison = null)
+    {
+        if (is_array($dbForeignId)) {
+            $useMinMax = false;
+            if (isset($dbForeignId['min'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbForeignId['max'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId, $comparison);
+    }
+
+    /**
+     * Filter the query on the file_id column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbFileId(1234); // WHERE file_id = 1234
+     * $query->filterByDbFileId(array(12, 34)); // WHERE file_id IN (12, 34)
+     * $query->filterByDbFileId(array('min' => 12)); // WHERE file_id >= 12
+     * $query->filterByDbFileId(array('max' => 12)); // WHERE file_id <= 12
+     * </code>
+     *
+     * @see       filterByCcPlayoutHistoryTemplate()
+     *
+     * @param     mixed $dbFileId The value to use as filter.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbFileId($dbFileId = null, $comparison = null)
+    {
+        if (is_array($dbFileId)) {
+            $useMinMax = false;
+            if (isset($dbFileId['min'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $dbFileId['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbFileId['max'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $dbFileId['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $dbFileId, $comparison);
+    }
+
+    /**
+     * Filter the query on the status column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbStatus('fooValue');   // WHERE status = 'fooValue'
+     * $query->filterByDbStatus('%fooValue%'); // WHERE status LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbStatus The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbStatus($dbStatus = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbStatus)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbStatus)) {
+                $dbStatus = str_replace('*', '%', $dbStatus);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::STATUS, $dbStatus, $comparison);
+    }
+
+    /**
+     * Filter the query by a related CcPlayoutHistoryTemplate object
+     *
+     * @param   CcPlayoutHistoryTemplate|PropelObjectCollection $ccPlayoutHistoryTemplate The related object(s) to use as filter
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return                 ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     * @throws PropelException - if the provided filter is invalid.
+     */
+    public function filterByCcPlayoutHistoryTemplate($ccPlayoutHistoryTemplate, $comparison = null)
+    {
+        if ($ccPlayoutHistoryTemplate instanceof CcPlayoutHistoryTemplate) {
+            return $this
+                ->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $ccPlayoutHistoryTemplate->getDbId(), $comparison);
+        } elseif ($ccPlayoutHistoryTemplate instanceof PropelObjectCollection) {
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+
+            return $this
+                ->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $ccPlayoutHistoryTemplate->toKeyValue('PrimaryKey', 'DbId'), $comparison);
+        } else {
+            throw new PropelException('filterByCcPlayoutHistoryTemplate() only accepts arguments of type CcPlayoutHistoryTemplate or PropelCollection');
+        }
+    }
+
+    /**
+     * Adds a JOIN clause to the query using the CcPlayoutHistoryTemplate relation
+     *
+     * @param     string $relationAlias optional alias for the relation
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function joinCcPlayoutHistoryTemplate($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        $tableMap = $this->getTableMap();
+        $relationMap = $tableMap->getRelation('CcPlayoutHistoryTemplate');
+
+        // create a ModelJoin object for this join
+        $join = new ModelJoin();
+        $join->setJoinType($joinType);
+        $join->setRelationMap($relationMap, $this->useAliasInSQL ? $this->getModelAlias() : null, $relationAlias);
+        if ($previousJoin = $this->getPreviousJoin()) {
+            $join->setPreviousJoin($previousJoin);
+        }
+
+        // add the ModelJoin to the current object
+        if ($relationAlias) {
+            $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
+            $this->addJoinObject($join, $relationAlias);
+        } else {
+            $this->addJoinObject($join, 'CcPlayoutHistoryTemplate');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Use the CcPlayoutHistoryTemplate relation CcPlayoutHistoryTemplate object
+     *
+     * @see       useQuery()
+     *
+     * @param     string $relationAlias optional alias for the relation,
+     *                                   to be used as main alias in the secondary query
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return   CcPlayoutHistoryTemplateQuery A secondary query class using the current class as primary query
+     */
+    public function useCcPlayoutHistoryTemplateQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        return $this
+            ->joinCcPlayoutHistoryTemplate($relationAlias, $joinType)
+            ->useQuery($relationAlias ? $relationAlias : 'CcPlayoutHistoryTemplate', 'CcPlayoutHistoryTemplateQuery');
+    }
+
+    /**
+     * Exclude object from result
+     *
+     * @param   ThirdPartyTrackReferences $thirdPartyTrackReferences Object to remove from the list of results
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function prune($thirdPartyTrackReferences = null)
+    {
+        if ($thirdPartyTrackReferences) {
+            $this->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $thirdPartyTrackReferences->getDbId(), Criteria::NOT_EQUAL);
+        }
+
+        return $this;
+    }
+
+}
diff --git a/airtime_mvc/application/services/CalendarService.php b/airtime_mvc/application/services/CalendarService.php
index ed879e90d..098aeaa74 100644
--- a/airtime_mvc/application/services/CalendarService.php
+++ b/airtime_mvc/application/services/CalendarService.php
@@ -55,23 +55,6 @@ class Application_Service_CalendarService
                         "icon" => "overview",
                         "url" => $baseUrl."library/edit-file-md/id/".$ccFile->getDbId());
                 }
-
-                //recorded show can be uploaded to soundcloud
-                if (Application_Model_Preference::GetUploadToSoundcloudOption()) {
-                    $scid = $ccFile->getDbSoundcloudId();
-
-                    if ($scid > 0) {
-                        $menu["soundcloud_view"] = array(
-                            "name" => _("View on Soundcloud"),
-                            "icon" => "soundcloud",
-                            "url" => $ccFile->getDbSoundcloudLinkToFile());
-                    }
-
-                    $text = is_null($scid) ? _('Upload to SoundCloud') : _('Re-upload to SoundCloud');
-                    $menu["soundcloud_upload"] = array(
-                        "name"=> $text,
-                        "icon" => "soundcloud");
-                }
             } else {
                 $menu["content"] = array(
                     "name"=> _("Show Content"),
diff --git a/airtime_mvc/application/services/SoundCloudService.php b/airtime_mvc/application/services/SoundCloudService.php
new file mode 100644
index 000000000..ec36ec5c1
--- /dev/null
+++ b/airtime_mvc/application/services/SoundCloudService.php
@@ -0,0 +1,164 @@
+<?php
+
+require_once "ThirdPartyService.php";
+
+class SoundcloudService extends ThirdPartyService {
+
+    /**
+     * @var Soundcloud\Service SoundCloud API wrapper object
+     */
+    private $_client;
+
+    /**
+     * @var string service name to store in ThirdPartyTrackReferences database
+     */
+    protected $_SERVICE_NAME = 'SoundCloud';
+
+    /**
+     * @var string base URI for SoundCloud tracks
+     */
+    protected $_THIRD_PARTY_TRACK_URI = 'http://api.soundcloud.com/tracks/';
+
+    /**
+     * @var array Application_Model_Preference functions for SoundCloud and their
+     *            associated API parameter keys so that we can call them dynamically
+     */
+    private $_SOUNDCLOUD_PREF_FUNCTIONS = array(
+        "getDefaultSoundCloudLicenseType" => "license",
+        "getDefaultSoundCloudSharingType" => "sharing"
+    );
+
+    /**
+     * Initialize the service
+     */
+    public function __construct() {
+        $CC_CONFIG      = Config::getConfig();
+        // FIXME: These values are hardcoded into conf.php right now...
+        // we should move these to a global config file
+        $clientId       = $CC_CONFIG['soundcloud-client-id'];
+        $clientSecret   = $CC_CONFIG['soundcloud-client-secret'];
+        $baseUrl        = $CC_CONFIG['baseUrl'] . ":" . $CC_CONFIG['basePort'];
+        $redirectUri    = 'http://' . $baseUrl . '/soundcloud/redirect';
+
+        $this->_client = new Soundcloud\Service($clientId, $clientSecret, $redirectUri);
+        $accessToken = Application_Model_Preference::getSoundCloudRequestToken();
+        if (!empty($accessToken)) {
+            $this->_client->setAccessToken($accessToken);
+        }
+    }
+
+    // TODO: upload functionality will be moved to python, this is just for testing
+    /**
+     * Upload the file with the given identifier to SoundCloud
+     *
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @throws Soundcloud\Exception\InvalidHttpResponseCodeException
+     *         thrown when the upload fails for any reason
+     */
+    public function upload($fileId) {
+        $file = Application_Model_StoredFile::RecallById($fileId);
+        try {
+            $track = json_decode($this->_client->post('tracks', $this->_buildTrackArray($file)));
+            parent::_createTrackReference($fileId, $track);
+        } catch(Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
+            Logging::info("Invalid request: " . $e->getMessage());
+            // We should only get here if we have an access token, so attempt to refresh
+            $this->accessTokenRefresh();
+        }
+    }
+
+    /**
+     * Build a parameter array for the track being uploaded to SoundCloud
+     *
+     * @param $file Application_Model_StoredFile the file being uploaded
+     *
+     * @return array the track array to send to SoundCloud
+     */
+    private function _buildTrackArray($file) {
+        $trackArray = array(
+            'track[title]' => $file->getName(),
+            // TODO: verify that S3 uploads work
+            'track[asset_data]' => '@'.$file->getFilePaths()[0]
+        );
+
+        foreach($this->_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
+            $val = Application_Model_Preference::$func();
+            if (!empty($val)) {
+                $trackArray["track[$param]"] = $val;
+            }
+        }
+
+        return $trackArray;
+    }
+
+    /**
+     * Given a CcFiles identifier for a file that's been uploaded to SoundCloud,
+     * return a link to the remote file
+     *
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @return string the link to the remote file
+     */
+    public function getLinkToFile($fileId) {
+        $serviceId = $this->getServiceId($fileId);
+        // If we don't find a record for the file we'll get 0 back for the id
+        if ($serviceId == 0) { return ''; }
+        $track = json_decode($this->_client->get('tracks/'. $serviceId));
+        return $track->permalink_url;
+    }
+
+    /**
+     * Check whether an access token exists for the SoundCloud client
+     *
+     * @return bool true if an access token exists, otherwise false
+     */
+    public function hasAccessToken() {
+        $accessToken = $this->_client->getAccessToken();
+        return !empty($accessToken);
+    }
+
+    /**
+     * Get the SoundCloud authorization URL
+     *
+     * @return string the authorization URL
+     */
+    public function getAuthorizeUrl() {
+        // Pass the current URL in the state parameter in order to preserve it
+        // in the redirect. This allows us to create a singular script to redirect
+        // back to any station the request comes from.
+        $url = urlencode('http'.(empty($_SERVER['HTTPS'])?'':'s').'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
+        return $this->_client->getAuthorizeUrl(array("state" => $url));
+    }
+
+    /**
+     * Request a new access token from SoundCloud and store it in CcPref
+     *
+     * @param $code string exchange authorization code for access token
+     */
+    public function requestNewAccessToken($code) {
+        // Get a non-expiring access token
+        $response = $this->_client->accessToken($code, $postData = array('scope' => 'non-expiring'));
+        $accessToken = $response['access_token'];
+        Application_Model_Preference::setSoundCloudRequestToken($accessToken);
+    }
+
+    /**
+     * Regenerate the SoundCloud client's access token
+     *
+     * @throws Soundcloud\Exception\InvalidHttpResponseCodeException
+     *         thrown when attempting to regenerate a stale token
+     */
+    public function accessTokenRefresh() {
+        assert($this->hasAccessToken());
+        try {
+            $accessToken = $this->_client->getAccessToken();
+            $this->_client->accessTokenRefresh($accessToken);
+        } catch(Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
+            // If we get here, then that means our token is stale, so remove it
+            // Because we're using non-expiring tokens, we shouldn't get here (!)
+            Application_Model_Preference::setSoundCloudRequestToken("");
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
new file mode 100644
index 000000000..48e882a44
--- /dev/null
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * Class ThirdPartyService generic superclass for third-party services
+ */
+abstract class ThirdPartyService {
+
+    /**
+     * @var string service name to store in ThirdPartyTrackReferences database
+     */
+    protected $_SERVICE_NAME = '';
+
+    /**
+     * @var string base URI for third-party tracks
+     */
+    protected $_THIRD_PARTY_TRACK_URI = '';
+
+    /**
+     * Upload the file with the given identifier to a third-party service
+     *
+     * @param int $fileId the local CcFiles identifier
+     */
+    abstract function upload($fileId);
+
+    /**
+     * Create a ThirdPartyTrackReferences and save it to the database
+     *
+     * @param $fileId int    local CcFiles identifier
+     * @param $track  object third-party service track object
+     *
+     * @throws Exception
+     * @throws PropelException
+     */
+    protected function _createTrackReference($fileId, $track) {
+        // First, check if the track already has an entry in the database
+        $ref = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService($this->_SERVICE_NAME)
+            ->findOneByDbFileId($fileId);
+        if (is_null($ref)) {
+            $ref = new ThirdPartyTrackReferences();
+        }
+        $ref->setDbService($this->_SERVICE_NAME);
+        $ref->setDbForeignId($track->id);
+        $ref->setDbFileId($fileId);
+        $ref->setDbStatus($track->state);
+        $ref->save();
+    }
+
+    /**
+     * Remove a ThirdPartyTrackReferences from the database.
+     * This is necessary if the track was removed from the service
+     * or the foreign id in our database is incorrect
+     *
+     * @param $fileId int local CcFiles identifier
+     *
+     * @throws Exception
+     * @throws PropelException
+     */
+    public function removeTrackReference($fileId) {
+        $ref = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService($this->_SERVICE_NAME)
+            ->findOneByDbFileId($fileId);
+        $ref->delete();
+    }
+
+    /**
+     * Given a CcFiles identifier for a file that's been uploaded to a third-party service,
+     * return the third-party identifier for the remote file
+     *
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @return int the service foreign identifier
+     */
+    public function getServiceId($fileId) {
+        $ref = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService($this->_SERVICE_NAME)
+            ->findOneByDbFileId($fileId); // There shouldn't be duplicates!
+        return is_null($ref) ? 0 : $ref->getDbForeignId();
+    }
+
+    /**
+     * Given a CcFiles identifier for a file that's been uploaded to a third-party service,
+     * return a link to the remote file
+     *
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @return string the link to the remote file
+     */
+    public function getLinkToFile($fileId) {
+        $serviceId = $this->getServiceId($fileId);
+        return $serviceId > 0 ? $this->_THIRD_PARTY_TRACK_URI . $serviceId : '';
+    }
+
+    /**
+     * Check whether an OAuth access token exists for the third-party client
+     *
+     * @return bool true if an access token exists, otherwise false
+     */
+    abstract function hasAccessToken();
+
+    /**
+     * Get the OAuth authorization URL
+     *
+     * @return string the authorization URL
+     */
+    abstract function getAuthorizeUrl();
+
+    /**
+     * Request a new OAuth access token from a third-party service and store it in CcPref
+     *
+     * @param $code string exchange authorization code for access token
+     */
+    abstract function requestNewAccessToken($code);
+
+    /**
+     * Regenerate the third-party client's OAuth access token
+     */
+    abstract function accessTokenRefresh();
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 9468753d6..adb54002c 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -1,5 +1,25 @@
 <?php
 
+/**
+ * Check if a given classname belongs to a subclass of AirtimeUpgrader
+ *
+ * @param $c string class name
+ *
+ * @return bool true if the $c is a subclass of AirtimeUpgrader
+ */
+function isUpgrade($c) {
+    return is_subclass_of($c, "AirtimeUpgrader");
+}
+
+/**
+ * Filter all declared classes to get all upgrade classes dynamically
+ *
+ * @return array all upgrade classes
+ */
+function getUpgrades() {
+    return array_filter(get_declared_classes(), "isUpgrade");
+}
+
 abstract class AirtimeUpgrader
 {
     /** Versions that this upgrader class can upgrade from (an array of version strings). */
@@ -9,27 +29,27 @@ abstract class AirtimeUpgrader
 
     public static function getCurrentVersion()
     {
-        CcPrefPeer::clearInstancePool(); //Ensure we don't get a cached Propel object (cached DB results) 
-                                         //because we're updating this version number within this HTTP request as well.
+        CcPrefPeer::clearInstancePool(); //Ensure we don't get a cached Propel object (cached DB results)
+        //because we're updating this version number within this HTTP request as well.
         $pref = CcPrefQuery::create()
-        ->filterByKeystr('system_version')
-        ->findOne();
+            ->filterByKeystr('system_version')
+            ->findOne();
         $airtime_version = $pref->getValStr();
         return $airtime_version;
     }
-    
-    /** 
+
+    /**
      * This function checks to see if this class can perform an upgrade of your version of Airtime
      * @return boolean True if we can upgrade your version of Airtime.
      */
     public function checkIfUpgradeSupported()
-    {        
+    {
         if (!in_array(AirtimeUpgrader::getCurrentVersion(), $this->getSupportedVersions())) {
             return false;
         }
         return true;
     }
-    
+
     protected function toggleMaintenanceScreen($toggle)
     {
         if ($toggle)
@@ -51,7 +71,7 @@ abstract class AirtimeUpgrader
             }*/
         }
     }
-            
+
     /** Implement this for each new version of Airtime */
     abstract public function upgrade();
 }
@@ -437,3 +457,49 @@ class AirtimeUpgrader2512 extends AirtimeUpgrader
 
     }
 }
+
+class AirtimeUpgrader2513 extends AirtimeUpgrader
+{
+    protected function getSupportedVersions() {
+        return array (
+            '2.5.12'
+        );
+    }
+
+    public function getNewVersion() {
+        return '2.5.13';
+    }
+
+    public function upgrade($dir = __DIR__) {
+        Cache::clear();
+        assert($this->checkIfUpgradeSupported());
+
+        $newVersion = $this->getNewVersion();
+
+        try {
+            $this->toggleMaintenanceScreen(true);
+            Cache::clear();
+
+            // Begin upgrade
+            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
+            $values = parse_ini_file($airtimeConf, true);
+
+            $username = $values['database']['dbuser'];
+            $password = $values['database']['dbpass'];
+            $host = $values['database']['host'];
+            $database = $values['database']['dbname'];
+
+            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
+                     .$newVersion."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
+
+            Application_Model_Preference::SetAirtimeVersion($newVersion);
+            Cache::clear();
+
+            $this->toggleMaintenanceScreen(false);
+        } catch(Exception $e) {
+            $this->toggleMaintenanceScreen(false);
+            throw $e;
+        }
+    }
+}
+
diff --git a/airtime_mvc/application/views/scripts/error/error-404.phtml b/airtime_mvc/application/views/scripts/error/error-404.phtml
new file mode 100644
index 000000000..a63d44af8
--- /dev/null
+++ b/airtime_mvc/application/views/scripts/error/error-404.phtml
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN";
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <title><?php echo _("An error has occurred.") ?></title>
+  <?php echo $this->headLink(); ?>
+</head>
+<body>
+    <div class="error-content" id="error-404">
+        <h2><?php echo _("Page not found!")?></h2>
+        <p><?php echo _("We couldn't find the page you were looking for.")?></p>
+        <div class="button-bar">
+            <a class="toggle-button" href="<?php echo $this->baseUrl('dashboard/help'); ?>"><?php echo _("Help") ?></a>
+        </div>
+    </div>
+</body>
+</html>
diff --git a/airtime_mvc/application/views/scripts/form/preferences.phtml b/airtime_mvc/application/views/scripts/form/preferences.phtml
index d200bf627..681cdfdd5 100644
--- a/airtime_mvc/application/views/scripts/form/preferences.phtml
+++ b/airtime_mvc/application/views/scripts/form/preferences.phtml
@@ -11,6 +11,12 @@
 
     <?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') ?>
diff --git a/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml b/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml
index 4ee3ff7f0..594468474 100644
--- a/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml
+++ b/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml
@@ -1,112 +1,30 @@
 <fieldset class="padded">
     <dl class="zend_form">
-        <dd id="UploadToSoundcloudOption-element" class="block-display" style=" margin:6px 0 10px 0">
-            <label class="optional" for="UploadToSoundcloudOption">
-                <?php echo $this->element->getElement('UploadToSoundcloudOption') ?>
-                <strong><?php echo $this->element->getElement('UploadToSoundcloudOption')->getLabel() ?></strong>
-            </label>
-            <?php if($this->element->getElement('UploadToSoundcloudOption')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('UploadToSoundcloudOption')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?> 
-        </dd>
-        <dd id="SoundCloudDownloadbleOption-element" class="block-display" style="padding-left:20px; margin:6px 0 10px 0">
-            <label class="optional" for="SoundCloudDownloadbleOption">
-                <?php echo $this->element->getElement('SoundCloudDownloadbleOption') ?>
-                <strong><?php echo $this->element->getElement('SoundCloudDownloadbleOption')->getLabel() ?></strong>
-            </label>
-            <?php if($this->element->getElement('SoundCloudDownloadbleOption')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudDownloadbleOption')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?> 
-        </dd>
-        <dt id="SoundCloudUser-label" class="block-display">
-            <label class="optional" for="SoundCloudUser"><?php echo $this->element->getElement('SoundCloudUser')->getLabel() ?>
-                <span class="info-text-small"><?php echo _("(Required)")?></span> :
-            </label>
-        </dt>
-        <dd id="SoundCloudUser-element" class="block-display">
-            <?php echo $this->element->getElement('SoundCloudUser') ?>
-            <?php if($this->element->getElement('SoundCloudUser')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudUser')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?> 
-        </dd>
-        <dt id="SoundCloudPassword-label" class="block-display">
-            <label class="optional" for="SoundCloudPassword"><?php echo $this->element->getElement('SoundCloudPassword')->getLabel() ?>
-                <span class="info-text-small"><?php echo _("(Required)")?></span> :
-            </label>
-        </dt>
-        <dd id="SoundCloudPassword-element" class="block-display">
-            <?php echo $this->element->getElement('SoundCloudPassword') ?>
-            <?php if($this->element->getElement('SoundCloudPassword')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudPassword')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?> 
-        </dd>
-        <dt id="SoundCloudTags-label" class="block-display">
-            <label class="optional" for="SoundCloudTags"><?php echo $this->element->getElement('SoundCloudTags')->getLabel() ?></label>
-        </dt>
-        <dd id="SoundCloudTags-element" class="block-display clearfix">
-            <?php echo $this->element->getElement('SoundCloudTags') ?>
-            <?php if($this->element->getElement('SoundCloudTags')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudTags')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?>
-        </dd>
-        <dt id="SoundCloudGenre-label" class="block-display">
-            <label class="optional" for="SoundCloudGenre"><?php echo $this->element->getElement('SoundCloudGenre')->getLabel() ?></label>
-        </dt>
-        <dd id="SoundCloudGenre-element" class="block-display">
-            <?php echo $this->element->getElement('SoundCloudGenre') ?>
-             <?php if($this->element->getElement('SoundCloudGenre')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudGenre')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?>
-        </dd>
-        <dt id="SoundCloudTrackType-label" class="block-display">
-             <label class="optional" for="SoundCloudTrackType"><?php echo $this->element->getElement('SoundCloudTrackType')->getLabel() ?></label>
-        </dt>
-        <dd id="SoundCloudTrackType-element" class="block-display">
-            <?php echo $this->element->getElement('SoundCloudTrackType') ?>
-            <?php if($this->element->getElement('SoundCloudTrackType')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudTrackType')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?>
-        </dd>
-        <dt id="SoundCloudLicense-label" class="block-display">
-            <label class="optional" for="SoundCloudLicense"><?php echo $this->element->getElement('SoundCloudLicense')->getLabel() ?></label>
-        </dt>
-        <dd id="SoundCloudLicense-element" class="block-display">
-            <?php echo $this->element->getElement('SoundCloudLicense') ?>
-            <?php if($this->element->getElement('SoundCloudLicense')->hasErrors()) : ?>
-                <ul class='errors'>
-                    <?php foreach($this->element->getElement('SoundCloudLicense')->getMessages() as $error): ?>
-                        <li><?php echo $error; ?></li>
-                    <?php endforeach; ?>
-                </ul>
-            <?php endif; ?>
-        </dd>
+<!--        <dt id="SoundCloudTrackType-label" class="block-display">-->
+<!--             <label class="optional" for="SoundCloudTrackType">--><?php //echo $this->element->getElement('SoundCloudTrackType')->getLabel() ?><!--</label>-->
+<!--        </dt>-->
+<!--        <dd id="SoundCloudTrackType-element" class="block-display">-->
+<!--            --><?php //echo $this->element->getElement('SoundCloudTrackType') ?>
+<!--            --><?php //if($this->element->getElement('SoundCloudTrackType')->hasErrors()) : ?>
+<!--                <ul class='errors'>-->
+<!--                    --><?php //foreach($this->element->getElement('SoundCloudTrackType')->getMessages() as $error): ?>
+<!--                        <li>--><?php //echo $error; ?><!--</li>-->
+<!--                    --><?php //endforeach; ?>
+<!--                </ul>-->
+<!--            --><?php //endif; ?>
+<!--        </dd>-->
+
+        <?php
+            $soundcloudService = new SoundcloudService();
+            if ($soundcloudService->hasAccessToken()) {
+                echo $this->element->getElement('SoundCloudDisconnect')->render();
+            } else {
+                echo $this->element->getElement('SoundCloudConnect')->render();
+            }
+        ?>
+
+        <?php echo $this->element->getElement('SoundCloudLicense')->render() ?>
+
+        <?php echo $this->element->getElement('SoundCloudSharing')->render() ?>
     </dl>
 </fieldset>
diff --git a/airtime_mvc/build/build.properties b/airtime_mvc/build/build.properties
index ee5a5f8fa..556862bb1 100644
--- a/airtime_mvc/build/build.properties
+++ b/airtime_mvc/build/build.properties
@@ -1,6 +1,6 @@
 #Note: project.home is automatically generated by the propel-install script. 
 #Any manual changes to this value will be overwritten.
-project.home = /home/sourcefabric/dev/Airtime-SaaS/Airtime/airtime_mvc
+project.home = /home/sourcefabric/dev/Airtime/airtime_mvc
 project.build = ${project.home}/build
 
 #Database driver
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index ece788ea7..03bad21cb 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -531,4 +531,14 @@
       <reference local="template_id" foreign="id"/>
     </foreign-key>
   </table>
+  <table name="third_party_track_references" phpName="ThirdPartyTrackReferences">
+    <column name="id" phpName="DbId" primaryKey="true" type="INTEGER" autoIncrement="true" required="true" />
+    <column name="service" phpName="DbService" type="VARCHAR" size="512" required="true" />
+    <column name="foreign_id" phpName="DbForeignId" type="INTEGER" required="true" />
+    <column name="file_id" phpName="DbFileId" type="INTEGER" required="true" />
+    <column name="status" phpName="DbStatus" type="VARCHAR" size="256" required="true" />
+    <foreign-key foreignTable="cc_playout_history_template" name="track_reference_fkey" onDelete="CASCADE">
+      <reference local="file_id" foreign="id"/>
+    </foreign-key>
+  </table>
 </database>
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index 392539496..3d7aaf39a 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -670,6 +670,22 @@ CREATE TABLE "cc_playout_history_template_field"
     PRIMARY KEY ("id")
 );
 
+-----------------------------------------------------------------------
+-- third_party_track_references
+-----------------------------------------------------------------------
+
+DROP TABLE IF EXISTS "third_party_track_references" CASCADE;
+
+CREATE TABLE "third_party_track_references"
+(
+    "id" serial NOT NULL,
+    "service" VARCHAR(512) NOT NULL,
+    "foreign_id" INTEGER NOT NULL,
+    "file_id" INTEGER NOT NULL,
+    "status" VARCHAR(256) NOT NULL,
+    PRIMARY KEY ("id")
+);
+
 ALTER TABLE "cc_files" ADD CONSTRAINT "cc_files_owner_fkey"
     FOREIGN KEY ("owner_id")
     REFERENCES "cc_subjs" ("id");
@@ -831,3 +847,8 @@ ALTER TABLE "cc_playout_history_template_field" ADD CONSTRAINT "cc_playout_histo
     FOREIGN KEY ("template_id")
     REFERENCES "cc_playout_history_template" ("id")
     ON DELETE CASCADE;
+
+ALTER TABLE "third_party_track_references" ADD CONSTRAINT "track_reference_fkey"
+    FOREIGN KEY ("file_id")
+    REFERENCES "cc_playout_history_template" ("id")
+    ON DELETE CASCADE;
diff --git a/airtime_mvc/library/soundcloud-api/README.md b/airtime_mvc/library/soundcloud-api/README.md
deleted file mode 100644
index 68df3a21a..000000000
--- a/airtime_mvc/library/soundcloud-api/README.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# SoundCloud PHP API Wrapper
-
-## Introduction
-
-A wrapper for the SoundCloud API written in PHP with support for authentication using [OAuth 2.0](http://oauth.net/2/).
-
-The wrapper got a real overhaul with version 2.0. The current version was written with [PEAR](http://pear.php.net/) in mind and can easily by distributed as a PEAR package.
-
-## Getting started
-
-Check out the [getting started](https://github.com/mptre/php-soundcloud/wiki/OAuth-2) wiki entry for further reference on how to get started. Also make sure to check out the [demo application](https://github.com/mptre/ci-soundcloud) for some example code.
-
-
-## Examples
-
-The wrapper includes convenient methods used to perform HTTP requests on behalf of the authenticated user. Below you'll find a few quick examples.
-
-Ofcourse you need to handle the authentication first before being able to request and modify protect resources as demonstrated below. Therefor I refer to the [demo application](https://github.com/mptre/ci-soundcloud) which got some example code on how to handle authentication.
-
-### GET
-
-<pre><code>try {
-    $response = json_decode($soundcloud->get('me'), true);
-} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-    exit($e->getMessage());
-}</code></pre>
-
-### POST
-
-<pre><code>$comment = &lt;&lt;&lt;EOH
-&lt;comment&gt;
-    &lt;body&gt;Yeah!&lt;/body&gt;
-&lt;/comment&gt;
-EOH;
-
-try {
-    $response = json_decode(
-        $soundcloud->post(
-            'tracks/1/comments',
-            $comment,
-            array(CURLOPT_HTTPHEADER => array('Content-Type: application/xml'))
-        ),
-        true
-    );
-} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-    exit($e->getMessage());
-}</code></pre>
-
-### PUT
-
-<pre><code>$track = &lt;&lt;&lt;EOH
-&lt;track&gt;
-    &lt;downloadable&gt;true&lt;/downloadable&gt;
-&lt;/track&gt;
-EOH;
-
-try {
-    $response = json_decode(
-        $soundcloud->put(
-            'tracks/1',
-            $track,
-            array(CURLOPT_HTTPHEADER => array('Content-Type: application/xml'))
-        ),
-        true
-    );
-} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-    exit($e->getMessage());
-}</code></pre>
-
-### DELETE
-
-<pre><code>try {
-    $response = json_decode($soundcloud->delete('tracks/1'), true);
-} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-    exit($e->getMessage());
-}</code></pre>
-
-### DOWNLOAD TRACK
-
-<pre><code>try {
-    $track = $soundcloud->download(1337);
-} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-    exit($e->getMessage());
-}
-
-// do something clever with $track. Save to file perhaps?</code></pre>
-
-## Feedback and questions
-
-Found a bug or missing a feature? Don't hesitate to create a new issue here on GitHub. Or contact me [directly](https://github.com/mptre).
-
-Also make sure to check out the official [documentation](https://github.com/soundcloud/api/wiki/) and the join [Google Group](https://groups.google.com/group/soundcloudapi?pli=1) in order to stay updated.
-
-## License
-
-Copyright (c) 2011 Anton Lindqvist
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/airtime_mvc/library/soundcloud-api/Services/Soundcloud.php b/airtime_mvc/library/soundcloud-api/Services/Soundcloud.php
deleted file mode 100644
index 9eba8eade..000000000
--- a/airtime_mvc/library/soundcloud-api/Services/Soundcloud.php
+++ /dev/null
@@ -1,737 +0,0 @@
-<?php
-require_once 'Soundcloud/Exception.php';
-require_once 'Soundcloud/Version.php';
-
-/**
- * SoundCloud API wrapper with support for authentication using OAuth 2.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud {
-
-    /**
-     * Custom cURL option.
-     *
-     * @access public
-     *
-     * @var integer
-     */
-    const CURLOPT_OAUTH_TOKEN = 173;
-
-    /**
-     * Access token returned by the service provider after a successful authentication.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private $_accessToken;
-
-    /**
-     * Version of the API to use.
-     *
-     * @access private
-     *
-     * @var integer
-     */
-    private static $_apiVersion = 1;
-
-    /**
-     * Supported audio MIME types.
-     *
-     * @access private
-     *
-     * @var array
-     */
-    private static $_audioMimeTypes = array(
-        'aac' => 'video/mp4',
-        'aiff' => 'audio/x-aiff',
-        'flac' => 'audio/flac',
-        'mp3' => 'audio/mpeg',
-        'ogg' => 'audio/ogg',
-        'wav' => 'audio/x-wav'
-    );
-
-    /**
-     * OAuth client id.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private $_clientId;
-
-    /**
-     * OAuth client secret.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private $_clientSecret;
-
-    /**
-     * Development mode.
-     *
-     * @access private
-     *
-     * @var boolean
-     */
-     private $_development;
-
-    /**
-     * Available API domains.
-     *
-     * @access private
-     *
-     * @var array
-     */
-    private static $_domains = array(
-        'development' => 'sandbox-soundcloud.com',
-        'production' => 'soundcloud.com'
-    );
-
-    /**
-     * HTTP response body from the last request.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private $_lastHttpResponseBody;
-
-    /**
-     * HTTP response code from the last request.
-     *
-     * @access private
-     *
-     * @var integer
-     */
-    private $_lastHttpResponseCode;
-
-    /**
-     * HTTP response headers from last request.
-     *
-     * @access private
-     *
-     * @var array
-     */
-    private $_lastHttpResponseHeaders;
-
-    /**
-     * OAuth paths.
-     *
-     * @access private
-     *
-     * @var array
-     */
-    private static $_paths = array(
-        'authorize' => 'connect',
-        'access_token' => 'oauth2/token',
-    );
-
-    /**
-     * OAuth redirect uri.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private $_redirectUri;
-
-    /**
-     * API response format MIME type.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private $_requestFormat;
-
-    /**
-     * Available response formats.
-     *
-     * @access private
-     *
-     * @var array
-     */
-    private static $_responseFormats = array(
-        '*' => '*/*',
-        'json' => 'application/json',
-        'xml' => 'application/xml'
-    );
-
-    /**
-     * HTTP user agent.
-     *
-     * @access private
-     *
-     * @var string
-     */
-    private static $_userAgent = 'PHP-SoundCloud';
-
-    /**
-     * Class version.
-     *
-     * @var string
-     */
-    public $version;
-
-    /**
-     * Constructor.
-     *
-     * @param string $clientId OAuth client id
-     * @param string $clientSecret OAuth client secret
-     * @param string $redirectUri OAuth redirect uri
-     * @param boolean $development Sandbox mode
-     *
-     * @throws Services_Soundcloud_Missing_Client_Id_Exception when missing client id
-     * @return void
-     */
-    function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) {
-        if (empty($clientId)) {
-            throw new Services_Soundcloud_Missing_Client_Id_Exception();
-        }
-
-        $this->_clientId = $clientId;
-        $this->_clientSecret = $clientSecret;
-        $this->_redirectUri = $redirectUri;
-        $this->_development = $development;
-        $this->_responseFormat = self::$_responseFormats['json'];
-        $this->version = Services_Soundcloud_Version::get();
-    }
-
-    /**
-     * Get authorization URL.
-     *
-     * @param array $params Optional query string parameters
-     *
-     * @return string
-     * @see Soundcloud::_buildUrl()
-     */
-    function getAuthorizeUrl($params = array()) {
-        $defaultParams = array(
-            'client_id' => $this->_clientId,
-            'redirect_uri' => $this->_redirectUri,
-            'response_type' => 'code'
-        );
-        $params = array_merge($defaultParams, $params);
-
-        return $this->_buildUrl(self::$_paths['authorize'], $params, false);
-    }
-
-    /**
-     * Get access token URL.
-     *
-     * @param array $params Optional query string parameters
-     *
-     * @return string
-     * @see Soundcloud::_buildUrl()
-     */
-    function getAccessTokenUrl($params = array()) {
-        return $this->_buildUrl(self::$_paths['access_token'], $params, false);
-    }
-
-    /**
-     * Retrieve access token.
-     *
-     * @param string $code OAuth code returned from the service provider
-     * @param array $postData Optional post data
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_getAccessToken()
-     */
-    function accessToken($code, $postData = array(), $curlOptions = array()) {
-        $defaultPostData = array(
-            'code' => $code,
-            'client_id' => $this->_clientId,
-            'client_secret' => $this->_clientSecret,
-            'redirect_uri' => $this->_redirectUri,
-            'grant_type' => 'authorization_code'
-        );
-        $postData = array_merge($defaultPostData, $postData);
-
-        return $this->_getAccessToken($postData, $curlOptions);
-    }
-
-    /**
-     * Retrieve access token.
-     *
-     * @param string $username 
-     * @param string $password 
-     * @param array $postData Optional post data
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_getAccessToken()
-     */
-    function accessTokenResourceOwner($username, $password, $postData = array(), $curlOptions = array()) {
-        $defaultPostData = array(
-            'client_id' => $this->_clientId,
-            'client_secret' => $this->_clientSecret,
-            'grant_type' => 'password',
-            'username' => $username,
-            'password' => $password
-        );
-        $postData = array_merge($defaultPostData, $postData);
-
-        return $this->_getAccessToken($postData, $curlOptions);
-    }
-
-    /**
-     * Refresh access token.
-     *
-     * @param string $refreshToken
-     * @param array $postData Optional post data
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_getAccessToken()
-     */
-    function accessTokenRefresh($refreshToken, $postData = array(), $curlOptions = array()) {
-        $defaultPostData = array(
-            'refresh_token' => $refreshToken,
-            'client_id' => $this->_clientId,
-            'client_secret' => $this->_clientSecret,
-            'redirect_uri' => $this->_redirectUri,
-            'grant_type' => 'refresh_token'
-        );
-        $postData = array_merge($defaultPostData, $postData);
-
-        return $this->_getAccessToken($postData, $curlOptions);
-    }
-
-    /**
-     * Get access token.
-     *
-     * @return mixed
-     */
-    function getAccessToken() {
-        return $this->_accessToken;
-    }
-
-    /**
-     * Get API version.
-     *
-     * @return integer
-     */
-    function getApiVersion() {
-        return self::$_apiVersion;
-    }
-
-    /**
-     * Get the corresponding MIME type for a given file extension.
-     *
-     * @param string $extension
-     *
-     * @return string
-     * @throws Services_Soundcloud_Unsupported_Audio_Format_Exception if the format is unsupported
-     */
-    function getAudioMimeType($extension) {
-        if (array_key_exists($extension, self::$_audioMimeTypes)) {
-            return self::$_audioMimeTypes[$extension];
-        } else {
-            throw new Services_Soundcloud_Unsupported_Audio_Format_Exception();
-        }
-    }
-
-    /**
-     * Get development mode.
-     *
-     * @return boolean
-     */
-    function getDevelopment() {
-        return $this->_development;
-    }
-
-    /**
-     * Get HTTP response header.
-     *
-     * @param string $header Name of the header
-     *
-     * @return mixed
-     */
-    function getHttpHeader($header) {
-        if (is_array($this->_lastHttpResponseHeaders)
-            && array_key_exists($header, $this->_lastHttpResponseHeaders)
-        ) {
-            return $this->_lastHttpResponseHeaders[$header];
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Get redirect uri.
-     *
-     * @return mixed
-     */
-    function getRedirectUri() {
-        return $this->_redirectUri;
-    }
-
-    /**
-     * Get response format.
-     *
-     * @return string
-     */
-    function getResponseFormat() {
-        return $this->_responseFormat;
-    }
-
-    /**
-     * Set access token.
-     *
-     * @param string $accessToken
-     *
-     * @return object
-     */
-    function setAccessToken($accessToken) {
-        $this->_accessToken = $accessToken;
-
-        return $this;
-    }
-
-    /**
-     * Set redirect uri.
-     *
-     * @param string $redirectUri
-     *
-     * @return object
-     */
-    function setRedirectUri($redirectUri) {
-        $this->_redirectUri = $redirectUri;
-
-        return $this;
-    }
-
-    /**
-     * Set response format.
-     *
-     * @param string $format Could either be xml or json
-     *
-     * @throws Services_Soundcloud_Unsupported_Response_Format_Exception if the given response format isn't supported
-     * @return object
-     */
-    function setResponseFormat($format) {
-        if (array_key_exists($format, self::$_responseFormats)) {
-            $this->_responseFormat = self::$_responseFormats[$format];
-        } else {
-            throw new Services_Soundcloud_Unsupported_Response_Format_Exception();
-        }
-
-        return $this;
-    }
-
-    /**
-     * Set development mode.
-     *
-     * @param boolean $development
-     *
-     * @return object
-     */
-    function setDevelopment($development) {
-        $this->_development = $development;
-
-        return $this;
-    }
-
-    /**
-     * Send a GET HTTP request.
-     *
-     * @param string $path URI to request
-     * @param array $params Optional query string parameters
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_request()
-     */
-    function get($path, $params = array(), $curlOptions = array()) {
-        $url = $this->_buildUrl($path, $params);
-
-        return $this->_request($url, $curlOptions);
-    }
-
-    /**
-     * Send a POST HTTP request.
-     *
-     * @param string $path URI to request
-     * @param array $postData Optional post data
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_request()
-     */
-    function post($path, $postData = array(), $curlOptions = array()) {
-        $url = $this->_buildUrl($path);
-        $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
-        $options += $curlOptions;
-
-        return $this->_request($url, $options);
-    }
-
-    /**
-     * Send a PUT HTTP request.
-     *
-     * @param string $path URI to request
-     * @param array $postData Optional post data
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_request()
-     */
-    function put($path, $postData, $curlOptions = array()) {
-        $url = $this->_buildUrl($path);
-        $options = array(
-            CURLOPT_CUSTOMREQUEST => 'PUT',
-            CURLOPT_POSTFIELDS => $postData
-        );
-        $options += $curlOptions;
-
-        return $this->_request($url, $options);
-    }
-
-    /**
-     * Send a DELETE HTTP request.
-     *
-     * @param string $path URI to request
-     * @param array $params Optional query string parameters
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_request()
-     */
-    function delete($path, $params = array(), $curlOptions = array()) {
-        $url = $this->_buildUrl($path, $params);
-        $options = array(CURLOPT_CUSTOMREQUEST => 'DELETE');
-        $options += $curlOptions;
-
-        return $this->_request($url, $options);
-    }
-
-    /**
-     * Download track.
-     *
-     * @param integer $trackId
-     * @param array Optional query string parameters
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     * @see Soundcloud::_request()
-     */
-    function download($trackId, $params = array(), $curlOptions = array()) {
-        $lastResponseFormat = array_pop(
-            preg_split('/\//', $this->getResponseFormat())
-        );
-        $defaultParams = array('oauth_token' => $this->getAccessToken());
-        $defaultCurlOptions = array(
-            CURLOPT_FOLLOWLOCATION => true,
-            self::CURLOPT_OAUTH_TOKEN => false
-        );
-        $url = $this->_buildUrl(
-            'tracks/' . $trackId . '/download',
-            array_merge($defaultParams, $params)
-        );
-        $options = $defaultCurlOptions + $curlOptions;
-
-        $this->setResponseFormat('*');
-
-        $response = $this->_request($url, $options);
-
-        // rollback to the previously defined response format.
-        $this->setResponseFormat($lastResponseFormat);
-
-        return $response;
-    }
-
-    /**
-     * Construct default HTTP headers including response format and authorization.
-     *
-     * @param boolean Include access token or not
-     *
-     * @return array $headers
-     */
-    protected function _buildDefaultHeaders($includeAccessToken = true) {
-        $headers = array();
-
-        if ($this->_responseFormat) {
-            array_push($headers, 'Accept: ' . $this->_responseFormat);
-        }
-
-        if ($includeAccessToken && $this->_accessToken) {
-            array_push($headers, 'Authorization: OAuth ' . $this->_accessToken);
-        }
-
-        return $headers;
-    }
-
-    /**
-     * Construct a URL.
-     *
-     * @param string $path Relative or absolute URI
-     * @param array $params Optional query string parameters
-     * @param boolean $includeVersion Include API version
-     *
-     * @return string $url
-     */
-    protected function _buildUrl($path, $params = null, $includeVersion = true) {
-        if (preg_match('/^https?\:\/\//', $path)) {
-            $url = $path;
-        } else {
-            $url = 'https://';
-            $url .= (!preg_match('/connect/', $path)) ? 'api.' : '';
-            $url .= ($this->_development)
-                ? self::$_domains['development']
-                : self::$_domains['production'];
-            $url .= '/';
-            $url .= ($includeVersion) ? 'v' . self::$_apiVersion . '/' : '';
-            $url .= $path;
-        }
-
-        $url .= (count($params)) ? '?' . http_build_query($params) : '';
-
-        return $url;
-    }
-
-    /**
-     * Retrieve access token.
-     *
-     * @param array $postData Post data
-     * @param array $curlOptions Optional cURL options
-     *
-     * @return mixed
-     */
-    protected function _getAccessToken($postData, $curlOptions = array()) {
-        $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData);
-        $options += $curlOptions;
-        $response = json_decode(
-            $this->_request($this->getAccessTokenUrl(), $options),
-            true
-        );
-
-        if (array_key_exists('access_token', $response)) {
-            $this->_accessToken = $response['access_token'];
-
-            return $response;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Get HTTP user agent.
-     *
-     * @access protected
-     *
-     * @return string
-     */
-    protected function _getUserAgent() {
-        return self::$_userAgent . '/' . $this->version;
-    }
-
-    /**
-     * Parse HTTP response headers.
-     *
-     * @param string $headers
-     *
-     * @return array
-     */
-    protected function _parseHttpHeaders($headers) {
-        $headers = preg_split('/\n/', trim($headers));
-        $parsedHeaders = array();
-
-        foreach ($headers as $header) {
-            if (!preg_match('/\:\s/', $header)) {
-                continue;
-            }
-
-            list($key, $val) = preg_split('/\:\s/', $header, 2);
-            $key = str_replace('-', '_', strtolower($key));
-            $val = trim($val);
-
-            $parsedHeaders[$key] = $val;
-        }
-
-        return $parsedHeaders;
-    }
-
-    /**
-     * Validates HTTP response code.
-     *
-     * @access protected
-     *
-     * @return boolean
-     */
-    protected function _validResponseCode($code) {
-        return (bool)preg_match('/^20[0-9]{1}$/', $code);
-    }
-
-    /**
-     * Performs the actual HTTP request using curl. Can be overwritten by extending classes.
-     *
-     * @access protected
-     *
-     * @param string $url
-     * @param array $curlOptions Optional cURL options
-     *
-     * @throws Services_Soundcloud_Invalid_Http_Response_Code_Exception if the response code isn't valid
-     * @return mixed
-     */
-    protected function _request($url, $curlOptions = array()) {
-        $ch = curl_init();
-        $options = array(
-            CURLOPT_URL => $url,
-            CURLOPT_HEADER => true,
-            CURLOPT_RETURNTRANSFER => true,
-            CURLOPT_USERAGENT => $this->_getUserAgent()
-        );
-        $options += $curlOptions;
-
-        if (array_key_exists(self::CURLOPT_OAUTH_TOKEN, $options)) {
-            $includeAccessToken = $options[self::CURLOPT_OAUTH_TOKEN];
-            unset($options[self::CURLOPT_OAUTH_TOKEN]);
-        } else {
-            $includeAccessToken = true;
-        }
-
-        if (array_key_exists(CURLOPT_HTTPHEADER, $options)) {
-            $options[CURLOPT_HTTPHEADER] = array_merge(
-                $this->_buildDefaultHeaders(),
-                $curlOptions[CURLOPT_HTTPHEADER]
-            );
-        } else {
-            $options[CURLOPT_HTTPHEADER] = $this->_buildDefaultHeaders($includeAccessToken);
-        }
-
-        curl_setopt_array($ch, $options);
-
-        $data = curl_exec($ch);
-        $info = curl_getinfo($ch);
-
-        curl_close($ch);
-
-        $this->_lastHttpResponseHeaders = $this->_parseHttpHeaders(
-            substr($data, 0, $info['header_size'])
-        );
-        $this->_lastHttpResponseBody = substr($data, $info['header_size']);
-        $this->_lastHttpResponseCode = $info['http_code'];
-
-        if ($this->_validResponseCode($this->_lastHttpResponseCode)) {
-            return $this->_lastHttpResponseBody;
-        } else {
-            throw new Services_Soundcloud_Invalid_Http_Response_Code_Exception(
-                null,
-                0,
-                $this->_lastHttpResponseBody,
-                $this->_lastHttpResponseCode
-            );
-        }
-    }
-
-}
diff --git a/airtime_mvc/library/soundcloud-api/Services/Soundcloud/Exception.php b/airtime_mvc/library/soundcloud-api/Services/Soundcloud/Exception.php
deleted file mode 100644
index 76e3370ad..000000000
--- a/airtime_mvc/library/soundcloud-api/Services/Soundcloud/Exception.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-/**
- * Soundcloud missing client id exception.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud_Missing_Client_Id_Exception extends Exception {
-
-    /**
-     * Default message.
-     *
-     * @access protected
-     *
-     * @var string
-     */
-    protected $message = 'All requests must include a consumer key. Referred to as client_id in OAuth2.';
-
-}
-
-/**
- * Soundcloud invalid HTTP response code exception.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud_Invalid_Http_Response_Code_Exception extends Exception {
-
-    /**
-     * HTTP response body.
-     *
-     * @access protected
-     *
-     * @var string
-     */
-    protected $httpBody;
-
-    /**
-     * HTTP response code.
-     *
-     * @access protected
-     *
-     * @var integer
-     */
-    protected $httpCode;
-
-    /**
-     * Default message.
-     *
-     * @access protected
-     *
-     * @var string
-     */
-    protected $message = 'The requested URL responded with HTTP code %d.';
-
-    /**
-     * Constructor.
-     *
-     * @param string $message
-     * @param string $code
-     * @param string $httpBody
-     * @param integer $httpCode
-     *
-     * @return void
-     */
-    function __construct($message = null, $code = 0, $httpBody = null, $httpCode = 0) {
-        $this->httpBody = $httpBody;
-        $this->httpCode = $httpCode;
-        $message = sprintf($this->message, $httpCode);
-
-        parent::__construct($message, $code);
-    }
-
-    /**
-     * Get HTTP response body.
-     *
-     * @return mixed
-     */
-    function getHttpBody() {
-        return $this->httpBody;
-    }
-
-    /**
-     * Get HTTP response code.
-     *
-     * @return mixed
-     */
-    function getHttpCode() {
-        return $this->httpCode;
-    }
-
-}
-
-/**
- * Soundcloud unsupported response format exception.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud_Unsupported_Response_Format_Exception extends Exception {
-
-    /**
-     * Default message.
-     *
-     * @access protected
-     *
-     * @var string
-     */
-    protected $message = 'The given response format is unsupported.';
-
-}
-
-/**
- * Soundcloud unsupported audio format exception.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud_Unsupported_Audio_Format_Exception extends Exception {
-
-    /**
-     * Default message.
-     *
-     * @access protected
-     *
-     * @var string
-     */
-    protected $message = 'The given audio format is unsupported.';
-
-}
diff --git a/airtime_mvc/library/soundcloud-api/Services/Soundcloud/Version.php b/airtime_mvc/library/soundcloud-api/Services/Soundcloud/Version.php
deleted file mode 100644
index 6ee964a23..000000000
--- a/airtime_mvc/library/soundcloud-api/Services/Soundcloud/Version.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-/**
- * Soundcloud package version.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud_Version {
-
-    const MAJOR = 2;
-    const MINOR = 1;
-    const PATCH = 1;
-
-    public static function get() {
-        return implode('.', array(self::MAJOR, self::MINOR, self::PATCH));
-    }
-
-}
diff --git a/airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test.php b/airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test.php
deleted file mode 100644
index cfc3e9c4a..000000000
--- a/airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test.php
+++ /dev/null
@@ -1,310 +0,0 @@
-<?php
-require_once 'Soundcloud_Test_Helper.php';
-
-class Soundcloud_Test extends PHPUnit_Framework_TestCase {
-
-    protected $soundcloud;
-
-    function setUp() {
-        $this->soundcloud = new Services_Soundcloud_Expose(
-            '1337',
-            '1337',
-            'http://soundcloud.local/callback'
-        );
-    }
-
-    function tearDown() {
-        $this->soundcloud = null;
-    }
-
-    function testVersionFormat() {
-        $this->assertRegExp(
-            '/^[0-9]+\.[0-9]+\.[0-9]+$/',
-            Services_Soundcloud_Version::get()
-        );
-    }
-
-    function testGetUserAgent() {
-        $this->assertRegExp(
-            '/^PHP\-SoundCloud\/[0-9]+\.[0-9]+\.[0-9]+$/',
-            $this->soundcloud->getUserAgent()
-        );
-    }
-
-    function testApiVersion() {
-        $this->assertEquals(1, $this->soundcloud->getApiVersion());
-    }
-
-    function testGetAudioMimeTypes() {
-        $supportedExtensions = array(
-            'aac' => 'video/mp4',
-            'aiff' => 'audio/x-aiff',
-            'flac' => 'audio/flac',
-            'mp3' => 'audio/mpeg',
-            'ogg' => 'audio/ogg',
-            'wav' => 'audio/x-wav'
-        );
-        $unsupportedExtensions = array('gif', 'html', 'jpg', 'mp4', 'xml', 'xspf');
-
-        foreach ($supportedExtensions as $extension => $mimeType) {
-            $this->assertEquals(
-                $mimeType,
-                $this->soundcloud->getAudioMimeType($extension)
-            );
-        }
-
-        foreach ($unsupportedExtensions as $extension => $mimeType) {
-            $this->setExpectedException('Services_Soundcloud_Unsupported_Audio_Format_Exception');
-
-            $this->soundcloud->getAudioMimeType($extension);
-        }
-    }
-
-    function testGetAuthorizeUrl() {
-        $this->assertEquals(
-            'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code',
-            $this->soundcloud->getAuthorizeUrl()
-        );
-    }
-
-    function testGetAuthorizeUrlWithCustomQueryParameters() {
-        $this->assertEquals(
-            'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code&foo=bar',
-            $this->soundcloud->getAuthorizeUrl(array('foo' => 'bar'))
-        );
-
-        $this->assertEquals(
-            'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code&foo=bar&bar=foo',
-            $this->soundcloud->getAuthorizeUrl(array('foo' => 'bar', 'bar' => 'foo'))
-        );
-    }
-
-    function testGetAccessTokenUrl() {
-        $this->assertEquals(
-            'https://api.soundcloud.com/oauth2/token',
-            $this->soundcloud->getAccessTokenUrl()
-        );
-    }
-
-    function testSetAccessToken() {
-        $this->soundcloud->setAccessToken('1337');
-
-        $this->assertEquals('1337', $this->soundcloud->getAccessToken());
-    }
-
-    function testSetDevelopment() {
-        $this->soundcloud->setDevelopment(true);
-
-        $this->assertTrue($this->soundcloud->getDevelopment());
-    }
-
-    function testSetRedirectUri() {
-        $this->soundcloud->setRedirectUri('http://soundcloud.local/callback');
-
-        $this->assertEquals(
-            'http://soundcloud.local/callback',
-            $this->soundcloud->getRedirectUri()
-        );
-    }
-
-    function testDefaultResponseFormat() {
-        $this->assertEquals(
-            'application/json',
-            $this->soundcloud->getResponseFormat()
-        );
-    }
-
-    function testSetResponseFormatHtml() {
-        $this->setExpectedException('Services_Soundcloud_Unsupported_Response_Format_Exception');
-
-        $this->soundcloud->setResponseFormat('html');
-    }
-
-    function testSetResponseFormatAll() {
-        $this->soundcloud->setResponseFormat('*');
-
-        $this->assertEquals(
-            '*/*',
-            $this->soundcloud->getResponseFormat()
-        );
-    }
-
-    function testSetResponseFormatJson() {
-        $this->soundcloud->setResponseFormat('json');
-
-        $this->assertEquals(
-            'application/json',
-            $this->soundcloud->getResponseFormat()
-        );
-    }
-
-    function testSetResponseFormatXml() {
-        $this->soundcloud->setResponseFormat('xml');
-
-        $this->assertEquals(
-            'application/xml',
-            $this->soundcloud->getResponseFormat()
-        );
-    }
-
-    function testResponseCodeSuccess() {
-        $this->assertTrue($this->soundcloud->validResponseCode(200));
-    }
-
-    function testResponseCodeRedirect() {
-        $this->assertFalse($this->soundcloud->validResponseCode(301));
-    }
-
-    function testResponseCodeClientError() {
-        $this->assertFalse($this->soundcloud->validResponseCode(400));
-    }
-
-    function testResponseCodeServerError() {
-        $this->assertFalse($this->soundcloud->validResponseCode(500));
-    }
-
-    function testBuildDefaultHeaders() {
-        $this->assertEquals(
-            array('Accept: application/json'),
-            $this->soundcloud->buildDefaultHeaders()
-        );
-    }
-
-    function testBuildDefaultHeadersWithAccessToken() {
-        $this->soundcloud->setAccessToken('1337');
-
-        $this->assertEquals(
-            array('Accept: application/json', 'Authorization: OAuth 1337'),
-            $this->soundcloud->buildDefaultHeaders()
-        );
-    }
-
-    function testBuildUrl() {
-        $this->assertEquals(
-            'https://api.soundcloud.com/v1/me',
-            $this->soundcloud->buildUrl('me')
-        );
-    }
-
-    function testBuildUrlWithQueryParameters() {
-        $this->assertEquals(
-            'https://api.soundcloud.com/v1/tracks?q=rofl+dubstep',
-            $this->soundcloud->buildUrl(
-                'tracks',
-                array('q' => 'rofl dubstep')
-            )
-        );
-
-        $this->assertEquals(
-            'https://api.soundcloud.com/v1/tracks?q=rofl+dubstep&filter=public',
-            $this->soundcloud->buildUrl(
-                'tracks',
-                array('q' => 'rofl dubstep', 'filter' => 'public')
-            )
-        );
-    }
-
-    function testBuildUrlWithDevelopmentDomain() {
-        $this->soundcloud->setDevelopment(true);
-
-        $this->assertEquals(
-            'https://api.sandbox-soundcloud.com/v1/me',
-            $this->soundcloud->buildUrl('me')
-        );
-    }
-
-    function testBuildUrlWithoutApiVersion() {
-        $this->assertEquals(
-            'https://api.soundcloud.com/me',
-            $this->soundcloud->buildUrl('me', null, false)
-        );
-    }
-
-    function testBuildUrlWithAbsoluteUrl() {
-        $this->assertEquals(
-            'https://api.soundcloud.com/me',
-            $this->soundcloud->buildUrl('https://api.soundcloud.com/me')
-        );
-    }
-
-    /**
-     * @dataProvider dataProviderHttpHeaders
-     */
-    function testParseHttpHeaders($rawHeaders, $expectedHeaders) {
-        $parsedHeaders = $this->soundcloud->parseHttpHeaders($rawHeaders);
-
-        foreach ($parsedHeaders as $key => $val) {
-            $this->assertEquals($val, $expectedHeaders[$key]);
-        }
-    }
-
-    function testSoundcloudMissingConsumerKeyException() {
-        $this->setExpectedException('Services_Soundcloud_Missing_Client_Id_Exception');
-
-        $soundcloud = new Services_Soundcloud('', '');
-    }
-
-    function testSoundcloudInvalidHttpResponseCodeException() {
-        $this->setExpectedException('Services_Soundcloud_Invalid_Http_Response_Code_Exception');
-
-        $this->soundcloud->get('me');
-    }
-
-    /**
-     * @dataProvider dataProviderSoundcloudInvalidHttpResponseCode
-     */
-    function testSoundcloudInvalidHttpResponseCode($expectedHeaders) {
-        try {
-            $this->soundcloud->get('me');
-        } catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
-            $this->assertEquals(
-                '{"error":"401 - Unauthorized"}',
-                $e->getHttpBody()
-            );
-
-            $this->assertEquals(401, $e->getHttpCode());
-
-            foreach ($expectedHeaders as $key => $val) {
-                $this->assertEquals(
-                    $val,
-                    $this->soundcloud->getHttpHeader($key)
-                );
-            }
-        }
-    }
-
-    static function dataProviderHttpHeaders() {
-        $rawHeaders = <<<HEADERS
-HTTP/1.1 200 OK
-Date: Wed, 17 Nov 2010 15:39:52 GMT
-Cache-Control: public
-Content-Type: text/html; charset=utf-8
-Content-Encoding: gzip
-Server: foobar
-Content-Length: 1337
-HEADERS;
-        $expectedHeaders = array(
-            'date' => 'Wed, 17 Nov 2010 15:39:52 GMT',
-            'cache_control' => 'public',
-            'content_type' => 'text/html; charset=utf-8',
-            'content_encoding' => 'gzip',
-            'server' => 'foobar',
-            'content_length' => '1337'
-        );
-
-        return array(array($rawHeaders, $expectedHeaders));
-    }
-
-    static function dataProviderSoundcloudInvalidHttpResponseCode() {
-        $expectedHeaders = array(
-            'server' => 'nginx',
-            'content_type' => 'application/json; charset=utf-8',
-            'connection' => 'keep-alive',
-            'cache_control' => 'no-cache',
-            'content_length' => '30'
-        );
-
-        return array(array($expectedHeaders));
-    }
-
-}
diff --git a/airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test_Helper.php b/airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test_Helper.php
deleted file mode 100644
index 2959d0813..000000000
--- a/airtime_mvc/library/soundcloud-api/tests/Soundcloud_Test_Helper.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-set_include_path(
-    get_include_path()
-    . PATH_SEPARATOR
-    . realpath(dirname(__FILE__) . '/../')
-);
-
-require_once 'Services/Soundcloud.php';
-
-/**
- * Extended class of the Soundcloud class in order to expose protected methods
- * for testing.
- *
- * @category Services
- * @package Services_Soundcloud
- * @author Anton Lindqvist <anton@qvister.se>
- * @copyright 2010 Anton Lindqvist <anton@qvister.se>
- * @license http://www.opensource.org/licenses/mit-license.php MIT
- * @link http://github.com/mptre/php-soundcloud
- */
-class Services_Soundcloud_Expose extends Services_Soundcloud {
-
-    /**
-     * Class constructor. See parent constructor for further reference.
-     *
-     * @param string $clientId Application client id
-     * @param string $clientSecret Application client secret
-     * @param string $redirectUri Application redirect uri
-     * @param boolean $development Sandbox mode
-     *
-     * @return void
-     * @see Soundcloud
-     */
-    function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) {
-        parent::__construct($clientId, $clientSecret, $redirectUri, $development);
-    }
-
-    /**
-     * Construct default http headers including response format and authorization.
-     *
-     * @return array
-     * @see Soundcloud::_buildDefaultHeaders()
-     */
-    function buildDefaultHeaders() {
-        return $this->_buildDefaultHeaders();
-    }
-
-    /**
-     * Construct a url.
-     *
-     * @param string $path Relative or absolute uri
-     * @param array $params Optional query string parameters
-     * @param boolean $includeVersion Include the api version
-     *
-     * @return string
-     * @see Soundcloud::_buildUrl()
-     */
-    function buildUrl($path, $params = null, $includeVersion = true) {
-        return $this->_buildUrl($path, $params, $includeVersion);
-    }
-
-    /**
-     * Get http user agent.
-     *
-     * @return string
-     * @see Soundcloud::_getUserAgent()
-     */
-    function getUserAgent() {
-        return $this->_getUserAgent();
-    }
-
-    /**
-     * Parse HTTP response headers.
-     *
-     * @param string $headers
-     *
-     * @return array
-     * @see Soundcloud::_parseHttpHeaders()
-     */
-    function parseHttpHeaders($headers) {
-        return $this->_parseHttpHeaders($headers);
-    }
-
-    /**
-     * Validates http response code.
-     *
-     * @return boolean
-     * @see Soundcloud::_validResponseCode()
-     */
-    function validResponseCode($code) {
-        return $this->_validResponseCode($code);
-    }
-
-}
diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js
index dae2d916e..445e36b5a 100644
--- a/airtime_mvc/public/js/airtime/library/library.js
+++ b/airtime_mvc/public/js/airtime/library/library.js
@@ -612,17 +612,6 @@ var AIRTIME = (function(AIRTIME) {
             },
             "fnRowCallback": AIRTIME.library.fnRowCallback,
             "fnCreatedRow": function( nRow, aData, iDataIndex ) {
-                //add soundcloud icon
-                if (aData.soundcloud_id !== undefined) {
-                    if (aData.soundcloud_id === "-2") {
-                        $(nRow).find("td.library_title").append('<span class="small-icon progress"/>');
-                    } else if (aData.soundcloud_id === "-3") {
-                        $(nRow).find("td.library_title").append('<span class="small-icon sc-error"/>');
-                    } else if (aData.soundcloud_id !== null) {
-                        $(nRow).find("td.library_title").append('<span class="small-icon soundcloud"/>');
-                    }
-                }
-
                 // add checkbox
                 $(nRow).find('td.library_checkbox').html("<input type='checkbox' name='cb_"+aData.id+"'>");
 
@@ -892,10 +881,6 @@ var AIRTIME = (function(AIRTIME) {
             }
         });
        
-        checkLibrarySCUploadStatus();
-        
-        addQtipToSCIcons();
-       
         // begin context menu initialization.
         $.contextMenu({
             selector: '#library_display td:not(.library_checkbox)',
@@ -1026,21 +1011,19 @@ var AIRTIME = (function(AIRTIME) {
                     // add callbacks for Soundcloud menu items.
                     if (oItems.soundcloud !== undefined) {
                         var soundcloud = oItems.soundcloud.items;
-                        
+
                         // define an upload to soundcloud callback.
                         if (soundcloud.upload !== undefined) {
-                            
+
                             callback = function() {
-                                $.post(soundcloud.upload.url, function(){
-                                    addProgressIcon(data.id);
-                                });
+                                $.post(soundcloud.upload.url, function(){});
                             };
                             soundcloud.upload.callback = callback;
                         }
-                        
+
                         // define a view on soundcloud callback
                         if (soundcloud.view !== undefined) {
-                            
+
                             callback = function() {
                                 window.open(soundcloud.view.url);
                             };
@@ -1140,122 +1123,6 @@ function addProgressIcon(id) {
     }
 }
     
-function checkLibrarySCUploadStatus(){
-    var url = baseUrl+'Library/get-upload-to-soundcloud-status',
-        span,
-        id;
-    
-    function checkSCUploadStatusCallback(json) {
-        
-        if (json.sc_id > 0) {
-            span.removeClass("progress").addClass("soundcloud");
-            
-        }
-        else if (json.sc_id == "-3") {
-            span.removeClass("progress").addClass("sc-error");
-        }
-    }
-    
-    function checkSCUploadStatusRequest() {
-        
-        span = $(this);
-        id = span.parents("tr").data("aData").id;
-       
-        $.post(url, {format: "json", id: id, type:"file"}, checkSCUploadStatusCallback);
-    }
-    
-    $("#library_display span.progress").each(checkSCUploadStatusRequest);
-    setTimeout(checkLibrarySCUploadStatus, 5000);
-}
-    
-function addQtipToSCIcons() {
-    $("#content")
-    	.on('mouseover', ".progress, .soundcloud, .sc-error", function() {
-        
-    	var aData = $(this).parents("tr").data("aData"),
-        	id = aData.id,
-        	sc_id = aData.soundcloud_id;
-        
-        if ($(this).hasClass("progress")){
-            $(this).qtip({
-                content: {
-                    text: $.i18n._("Uploading in progress...")
-                },
-                position:{
-                    adjust: {
-                    resize: true,
-                    method: "flip flip"
-                    },
-                    at: "right center",
-                    my: "left top",
-                    viewport: $(window)
-                },
-                style: {
-                    classes: "ui-tooltip-dark file-md-long"
-                },
-                show: {
-                    ready: true // Needed to make it show on first mouseover event
-                }
-            });
-        }
-        else if ($(this).hasClass("soundcloud")){
-        	
-            $(this).qtip({
-                content: {
-                    text: $.i18n._("The soundcloud id for this file is: ") + sc_id
-                },
-                position:{
-                    adjust: {
-                    resize: true,
-                    method: "flip flip"
-                    },
-                    at: "right center",
-                    my: "left top",
-                    viewport: $(window)
-                },
-                style: {
-                    classes: "ui-tooltip-dark file-md-long"
-                },
-                show: {
-                    ready: true // Needed to make it show on first mouseover event
-                }
-            });
-        }
-        else if ($(this).hasClass("sc-error")) {
-            $(this).qtip({
-                content: {
-                    text: $.i18n._("Retreiving data from the server..."),
-                    ajax: {
-                        url: baseUrl+"Library/get-upload-to-soundcloud-status",
-                        type: "post",
-                        data: ({format: "json", id : id, type: "file"}),
-                        success: function(json, status){
-                            this.set('content.text', $.i18n._("There was an error while uploading to soundcloud.")+"<br>"+
-                                    $.i18n._("Error code: ")+json.error_code+
-                                    "<br>"+$.i18n._("Error msg: ")+json.error_msg+"<br>");
-                        }
-                    }
-                },
-                position:{
-                    adjust: {
-                    resize: true,
-                    method: "flip flip"
-                    },
-                    at: "right center",
-                    my: "left top",
-                    viewport: $(window)
-                },
-                style: {
-                    classes: "ui-tooltip-dark file-md-long"
-                },
-                show: {
-                    ready: true // Needed to make it show on first mouseover event
-                }
-            });
-        }
-    });
-}
-
 /*
  * This function is called from dataTables.columnFilter.js
  */
diff --git a/airtime_mvc/public/js/airtime/preferences/preferences.js b/airtime_mvc/public/js/airtime/preferences/preferences.js
index 252a64a99..ad4bfd5fb 100644
--- a/airtime_mvc/public/js/airtime/preferences/preferences.js
+++ b/airtime_mvc/public/js/airtime/preferences/preferences.js
@@ -82,6 +82,20 @@ function setTuneInSettingsReadonly() {
     }
 }
 
+function setSoundCloudSettingsListener() {
+    var connect = $("#SoundCloudConnect"),
+        disconnect = $("#SoundCloudDisconnect");
+    connect.click(function(e){
+        e.preventDefault();
+        window.location.replace(baseUrl + "soundcloud/authorize");
+    });
+
+    disconnect.click(function(e){
+        e.preventDefault();
+        window.location.replace(baseUrl + "soundcloud/deauthorize");
+    });
+}
+
 /*
  * Enable/disable mail server authentication fields
  */
@@ -118,21 +132,21 @@ function setCollapsibleWidgetJsCode() {
     $('#thirdPartyApi-element input').click(x);
 }
 
-function setSoundCloudCheckBoxListener() {
-    var subCheckBox= $("#UseSoundCloud,#SoundCloudDownloadbleOption");
-    var mainCheckBox= $("#UploadToSoundcloudOption");
-    subCheckBox.change(function(e){
-        if (subCheckBox.is(':checked')) {
-            mainCheckBox.attr("checked", true);
-        }
-    });
-
-    mainCheckBox.change(function(e){
-         if (!mainCheckBox.is(':checked')) {
-            $("#UseSoundCloud,#SoundCloudDownloadbleOption").attr("checked", false);
-        }   
-    });
-}
+//function setSoundCloudCheckBoxListener() {
+//    var subCheckBox= $("#UseSoundCloud,#SoundCloudDownloadbleOption");
+//    var mainCheckBox= $("#UploadToSoundcloudOption");
+//    subCheckBox.change(function(e){
+//        if (subCheckBox.is(':checked')) {
+//            mainCheckBox.attr("checked", true);
+//        }
+//    });
+//
+//    mainCheckBox.change(function(e){
+//         if (!mainCheckBox.is(':checked')) {
+//            $("#UseSoundCloud,#SoundCloudDownloadbleOption").attr("checked", false);
+//        }
+//    });
+//}
 
 function removeLogo() {
     $.post(baseUrl+'Preference/remove-logo', function(json){});
@@ -176,7 +190,7 @@ $(document).ready(function() {
 
     showErrorSections();
     
-    setSoundCloudCheckBoxListener();
+    //setSoundCloudCheckBoxListener();
     setMailServerInputReadonly();
     setSystemFromEmailReadonly();
     setConfigureMailServerListener();
@@ -184,4 +198,5 @@ $(document).ready(function() {
     setCollapsibleWidgetJsCode();
     setTuneInSettingsReadonly();
     setTuneInSettingsListener();
+    setSoundCloudSettingsListener();
 });
diff --git a/airtime_mvc/public/js/airtime/schedule/full-calendar-functions.js b/airtime_mvc/public/js/airtime/schedule/full-calendar-functions.js
index 0a3ae1b3b..d27478d4d 100644
--- a/airtime_mvc/public/js/airtime/schedule/full-calendar-functions.js
+++ b/airtime_mvc/public/js/airtime/schedule/full-calendar-functions.js
@@ -196,29 +196,29 @@ function eventRender(event, element, view) {
     }
    
     //add the record/rebroadcast/soundcloud icons if needed
-    if (event.record === 1) {
-        if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
-            if (event.soundcloud_id === -1) {
-                $(element).find(".fc-event-time").before('<span class="small-icon recording"></span>');
-            } else if ( event.soundcloud_id > 0) {
-                $(element).find(".fc-event-time").before('<span class="small-icon recording"></span><span class="small-icon soundcloud"></span>');
-            } else if (event.soundcloud_id === -2) {
-                $(element).find(".fc-event-time").before('<span class="small-icon recording"></span><span class="small-icon progress"></span>');
-            } else if (event.soundcloud_id === -3) {
-                $(element).find(".fc-event-time").before('<span class="small-icon recording"></span><span class="small-icon sc-error"></span>');
-            }
-        } else if (view.name === 'month') {
-            if(event.soundcloud_id === -1) {
-                $(element).find(".fc-event-title").after('<span class="small-icon recording"></span>');
-            } else if (event.soundcloud_id > 0) {
-                $(element).find(".fc-event-title").after('<span class="small-icon recording"></span><span class="small-icon soundcloud"></span>');
-            } else if (event.soundcloud_id === -2) {
-                $(element).find(".fc-event-title").after('<span class="small-icon recording"></span><span class="small-icon progress"></span>');
-            } else if (event.soundcloud_id === -3) {
-                $(element).find(".fc-event-title").after('<span class="small-icon recording"></span><span class="small-icon sc-error"></span>');
-            }
-        }
-    }
+    //if (event.record === 1) {
+    //    if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
+    //        if (event.soundcloud_id === -1) {
+    //            $(element).find(".fc-event-time").before('<span class="small-icon recording"></span>');
+    //        } else if ( event.soundcloud_id > 0) {
+    //            $(element).find(".fc-event-time").before('<span class="small-icon recording"></span><span class="small-icon soundcloud"></span>');
+    //        } else if (event.soundcloud_id === -2) {
+    //            $(element).find(".fc-event-time").before('<span class="small-icon recording"></span><span class="small-icon progress"></span>');
+    //        } else if (event.soundcloud_id === -3) {
+    //            $(element).find(".fc-event-time").before('<span class="small-icon recording"></span><span class="small-icon sc-error"></span>');
+    //        }
+    //    } else if (view.name === 'month') {
+    //        if(event.soundcloud_id === -1) {
+    //            $(element).find(".fc-event-title").after('<span class="small-icon recording"></span>');
+    //        } else if (event.soundcloud_id > 0) {
+    //            $(element).find(".fc-event-title").after('<span class="small-icon recording"></span><span class="small-icon soundcloud"></span>');
+    //        } else if (event.soundcloud_id === -2) {
+    //            $(element).find(".fc-event-title").after('<span class="small-icon recording"></span><span class="small-icon progress"></span>');
+    //        } else if (event.soundcloud_id === -3) {
+    //            $(element).find(".fc-event-title").after('<span class="small-icon recording"></span><span class="small-icon sc-error"></span>');
+    //        }
+    //    }
+    //}
 
     if (event.record === 0 && event.rebroadcast === 0) {
         if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
diff --git a/airtime_mvc/public/js/airtime/schedule/schedule.js b/airtime_mvc/public/js/airtime/schedule/schedule.js
index bf2937bcd..6bbefe0fa 100644
--- a/airtime_mvc/public/js/airtime/schedule/schedule.js
+++ b/airtime_mvc/public/js/airtime/schedule/schedule.js
@@ -63,24 +63,24 @@ function confirmCancelRecordedShow(show_instance_id){
     }
 }
 
-function uploadToSoundCloud(show_instance_id, el){
-    
-    var url = baseUrl+"Schedule/upload-to-sound-cloud",
-    	$el = $(el),
-    	$span = $el.find(".soundcloud");
-    
-    $.post(url, {id: show_instance_id, format: "json"});
-    
-    //first upload to soundcloud.
-    if ($span.length === 0){
-        $span = $("<span/>", {"class": "progress"});
-        
-        $el.find(".fc-event-title").after($span);
-    }
-    else {
-        $span.removeClass("soundcloud").addClass("progress");
-    }
-}
+//function uploadToSoundCloud(show_instance_id, el){
+//
+//    var url = baseUrl+"Schedule/upload-to-sound-cloud",
+//    	$el = $(el),
+//    	$span = $el.find(".soundcloud");
+//
+//    $.post(url, {id: show_instance_id, format: "json"});
+//
+//    //first upload to soundcloud.
+//    if ($span.length === 0){
+//        $span = $("<span/>", {"class": "progress"});
+//
+//        $el.find(".fc-event-title").after($span);
+//    }
+//    else {
+//        $span.removeClass("soundcloud").addClass("progress");
+//    }
+//}
 
 function checkCalendarSCUploadStatus(){
     var url = baseUrl+'Library/get-upload-to-soundcloud-status',
@@ -422,22 +422,22 @@ $(document).ready(function() {
                 }
                 
                 //define a soundcloud upload callback.
-                if (oItems.soundcloud_upload !== undefined) {
-                    
-                    callback = function() {
-                        uploadToSoundCloud(data.id, this.context);
-                    };
-                    oItems.soundcloud_upload.callback = callback;
-                }
+                //if (oItems.soundcloud_upload !== undefined) {
+                //
+                //    callback = function() {
+                //        uploadToSoundCloud(data.id, this.context);
+                //    };
+                //    oItems.soundcloud_upload.callback = callback;
+                //}
                 
                 //define a view on soundcloud callback.
-                if (oItems.soundcloud_view !== undefined) {
-                    
-                    callback = function() {
-                        window.open(oItems.soundcloud_view.url);
-                    };
-                    oItems.soundcloud_view.callback = callback;
-                }
+                //if (oItems.soundcloud_view !== undefined) {
+                //
+                //    callback = function() {
+                //        window.open(oItems.soundcloud_view.url);
+                //    };
+                //    oItems.soundcloud_view.callback = callback;
+                //}
                 
                 //define a cancel recorded show callback.
                 if (oItems.cancel_recorded !== undefined) {
diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js
index 8a57b4bd2..42e76f5e6 100644
--- a/airtime_mvc/public/js/airtime/showbuilder/builder.js
+++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js
@@ -533,7 +533,7 @@ var AIRTIME = (function(AIRTIME){
                     
                     if (aData.record === true) {
                         
-                        headerIcon =  (aData.soundcloud_id > 0) ? "soundcloud" : "recording";
+                        //headerIcon =  (aData.soundcloud_id > 0) ? "soundcloud" : "recording";
                         
                         $div = $("<div/>", {
                             "class": "small-icon " + headerIcon
diff --git a/composer.json b/composer.json
index 6d92538de..03adc292a 100644
--- a/composer.json
+++ b/composer.json
@@ -2,6 +2,7 @@
     "require": {
         "propel/propel1": "1.7.0-stable",
         "aws/aws-sdk-php": "2.7.9",
-        "raven/raven": "0.8.x-dev"
+        "raven/raven": "0.8.x-dev",
+        "ise/php-soundcloud": "3.0.1"
     }
 }
diff --git a/composer.lock b/composer.lock
index 17d9b1085..c819c7f39 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "30ad5215f679ce0ab55c7210b21a3b32",
+    "hash": "e731a5b93a15b54d4c22e26f33dc1aaa",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
@@ -165,6 +165,54 @@
             ],
             "time": "2014-08-11 04:32:36"
         },
+        {
+            "name": "ise/php-soundcloud",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/internalsystemerror/php-soundcloud.git",
+                "reference": "ac3ff2dce2a6e6d34636c58e66fd316d722c31df"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/internalsystemerror/php-soundcloud/zipball/ac3ff2dce2a6e6d34636c58e66fd316d722c31df",
+                "reference": "ac3ff2dce2a6e6d34636c58e66fd316d722c31df",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Soundcloud\\": "src/",
+                    "SoundcloudTest\\": "tests/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Anton Lindqvist",
+                    "email": "anton@qvister.se",
+                    "homepage": "http://qvister.se/",
+                    "role": "developer"
+                },
+                {
+                    "name": "Gary Lockett",
+                    "email": "ise@garylockett.com",
+                    "homepage": "http://www.garylockett.com/"
+                }
+            ],
+            "description": "API Wrapper for SoundCloud written in PHP with support for authentication using OAuth 2.0 by Anton Lindqvist (mptre), composer support added by Gary Lockett (ise)",
+            "homepage": "https://github.com/internalsystemerror/php-soundcloud",
+            "keywords": [
+                "soundcloud"
+            ],
+            "time": "2014-02-03 15:49:00"
+        },
         {
             "name": "phing/phing",
             "version": "2.9.1",
diff --git a/utils/soundcloud-uploader b/utils/soundcloud-uploader
deleted file mode 100755
index 5eab52b23..000000000
--- a/utils/soundcloud-uploader
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-#-------------------------------------------------------------------------------
-#   Copyright (c) 2011 Sourcefabric O.P.S.
-#
-#   This file is part of the Airtime project.
-#   http://airtime.sourcefabric.org/
-#
-#   Airtime is free software; you can redistribute it and/or modify
-#   it under the terms of the GNU General Public License as published by
-#   the Free Software Foundation; either version 2 of the License, or
-#   (at your option) any later version.
-#
-#   Airtime is distributed in the hope that it will be useful,
-#   but WITHOUT ANY WARRANTY; without even the implied warranty of
-#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#   GNU General Public License for more details.
-#
-#   You should have received a copy of the GNU General Public License
-#   along with Airtime; if not, write to the Free Software
-#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-#
-#-------------------------------------------------------------------------------
-#-------------------------------------------------------------------------------
-#  This script upload files to soundcloud
-#
-# Absolute path to this script
-SCRIPT=`readlink -f $0`
-# Absolute directory this script is in
-SCRIPTPATH=`dirname $SCRIPT`
-
-invokePwd=$PWD
-cd $SCRIPTPATH
-
-php -q soundcloud-uploader.php "$@" > /dev/null 2>&1 || exit 1
\ No newline at end of file
diff --git a/utils/soundcloud-uploader.php b/utils/soundcloud-uploader.php
deleted file mode 100644
index 0e1245945..000000000
--- a/utils/soundcloud-uploader.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-date_default_timezone_set("UTC");
-
-$values = parse_ini_file('/etc/airtime/airtime.conf', true);
-
-// Name of the web server user
-$CC_CONFIG['webServerUser'] = $values['general']['web_server_user'];
-$CC_CONFIG['phpDir'] = $values['general']['airtime_dir'];
-$CC_CONFIG['rabbitmq'] = $values['rabbitmq'];
-
-$CC_CONFIG['baseUrl'] = $values['general']['base_url'];
-$CC_CONFIG['basePort'] = $values['general']['base_port'];
-
-// Database config
-$CC_CONFIG['dsn']['username'] = $values['database']['dbuser'];
-$CC_CONFIG['dsn']['password'] = $values['database']['dbpass'];
-$CC_CONFIG['dsn']['hostspec'] = $values['database']['host'];
-$CC_CONFIG['dsn']['phptype'] = 'pgsql';
-$CC_CONFIG['dsn']['database'] = $values['database']['dbname'];
-
-$CC_CONFIG['soundcloud-connection-retries'] = $values['soundcloud']['connection_retries'];
-$CC_CONFIG['soundcloud-connection-wait'] = $values['soundcloud']['time_between_retries'];
-
-require_once($CC_CONFIG['phpDir'].'/application/configs/constants.php');
-require_once($CC_CONFIG['phpDir'].'/application/configs/conf.php');
-
-$CC_CONFIG['phpDir'] = $values['general']['airtime_dir'];
-
-// Ensure library/ is on include_path
-set_include_path(implode(PATH_SEPARATOR, array(
-        get_include_path(),
-        realpath($CC_CONFIG['phpDir'] . '/library')
-)));
-
-require_once($CC_CONFIG['phpDir'].'/application/common/Database.php');
-require_once($CC_CONFIG['phpDir'].'/application/models/StoredFile.php');
-require_once($CC_CONFIG['phpDir'].'/application/models/Preference.php');
-require_once($CC_CONFIG['phpDir'].'/application/models/MusicDir.php');
-require_once($CC_CONFIG['phpDir'].'/application/common/OsPath.php');
-
-set_include_path($CC_CONFIG['phpDir'].'/library' . PATH_SEPARATOR . get_include_path());
-require_once($CC_CONFIG['phpDir'].'/application/models/Soundcloud.php');
-
-set_include_path($CC_CONFIG['phpDir']."/application/models" . PATH_SEPARATOR . get_include_path());
-require_once($CC_CONFIG['phpDir']."/library/propel/runtime/lib/Propel.php");
-Propel::init($CC_CONFIG['phpDir']."/application/configs/airtime-conf.php");
-
-require_once 'propel/runtime/lib/Propel.php';
-Propel::init($CC_CONFIG['phpDir']."/application/configs/airtime-conf-production.php");
-
-if(count($argv) != 2){
-    exit;
-}
-
-$id = $argv[1];
-
-$file = Application_Model_StoredFile::RecallById($id);
-// set id with -2 which is indicator for processing
-$file->setSoundCloudFileId(SOUNDCLOUD_PROGRESS);
-$file->uploadToSoundCloud();
-

From 63b726e6b249a22ed59a35eda012b5f6d73f34c4 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 4 Jun 2015 11:38:46 -0400
Subject: [PATCH 02/39] SAAS-848 - Modify SoundCloud code to use external
 redirect

---
 airtime_mvc/application/configs/conf.php               | 1 +
 airtime_mvc/application/services/SoundCloudService.php | 7 ++-----
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php
index 27c1a0071..afc1d5f07 100644
--- a/airtime_mvc/application/configs/conf.php
+++ b/airtime_mvc/application/configs/conf.php
@@ -99,6 +99,7 @@ class Config {
         $globalAirtimeConfigValues = parse_ini_file($globalAirtimeConfig, true);
         $CC_CONFIG['soundcloud-client-id'] = $globalAirtimeConfigValues['soundcloud']['soundcloud_client_id'];
         $CC_CONFIG['soundcloud-client-secret'] = $globalAirtimeConfigValues['soundcloud']['soundcloud_client_secret'];
+        $CC_CONFIG['soundcloud-redirect-uri'] = $globalAirtimeConfigValues['soundcloud']['soundcloud_redirect_uri'];
 
         if(isset($values['demo']['demo'])){
             $CC_CONFIG['demo'] = $values['demo']['demo'];
diff --git a/airtime_mvc/application/services/SoundCloudService.php b/airtime_mvc/application/services/SoundCloudService.php
index ec36ec5c1..61895ca59 100644
--- a/airtime_mvc/application/services/SoundCloudService.php
+++ b/airtime_mvc/application/services/SoundCloudService.php
@@ -33,12 +33,9 @@ class SoundcloudService extends ThirdPartyService {
      */
     public function __construct() {
         $CC_CONFIG      = Config::getConfig();
-        // FIXME: These values are hardcoded into conf.php right now...
-        // we should move these to a global config file
         $clientId       = $CC_CONFIG['soundcloud-client-id'];
         $clientSecret   = $CC_CONFIG['soundcloud-client-secret'];
-        $baseUrl        = $CC_CONFIG['baseUrl'] . ":" . $CC_CONFIG['basePort'];
-        $redirectUri    = 'http://' . $baseUrl . '/soundcloud/redirect';
+        $redirectUri    = $CC_CONFIG['soundcloud-redirect-uri'];
 
         $this->_client = new Soundcloud\Service($clientId, $clientSecret, $redirectUri);
         $accessToken = Application_Model_Preference::getSoundCloudRequestToken();
@@ -127,7 +124,7 @@ class SoundcloudService extends ThirdPartyService {
         // Pass the current URL in the state parameter in order to preserve it
         // in the redirect. This allows us to create a singular script to redirect
         // back to any station the request comes from.
-        $url = urlencode('http'.(empty($_SERVER['HTTPS'])?'':'s').'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
+        $url = urlencode('http'.(empty($_SERVER['HTTPS'])?'':'s').'://'.$_SERVER['HTTP_HOST'].'/soundcloud/redirect');
         return $this->_client->getAuthorizeUrl(array("state" => $url));
     }
 

From 80bfb7fd93b6f1bec6e973a7020587da3cb1fc7a Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 4 Jun 2015 12:01:04 -0400
Subject: [PATCH 03/39] SAAS-848 - Add soundcloud_callback file

---
 airtime_mvc/public/soundcloud_callback.php | 28 ++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 airtime_mvc/public/soundcloud_callback.php

diff --git a/airtime_mvc/public/soundcloud_callback.php b/airtime_mvc/public/soundcloud_callback.php
new file mode 100644
index 000000000..e3bdb47dc
--- /dev/null
+++ b/airtime_mvc/public/soundcloud_callback.php
@@ -0,0 +1,28 @@
+<?php
+
+class Router {
+
+    /**
+     * Parse the URL query string and store the key->val pairs
+     * into an array, then redirect
+     */
+    public function reroute() {
+        $params = array();
+        parse_str($_SERVER['QUERY_STRING'], $params);
+        $this->_redirect($params);
+    }
+
+    /**
+     * Redirect to the URL passed in the 'state' parameter
+     * when we're redirected here from SoundCloud
+     *
+     * @param $params array array of URL query parameters
+     */
+    private function _redirect($params) {
+        $url = urldecode($params['state']);
+        header("Location: $url?" . $_SERVER['QUERY_STRING']);
+    }
+
+}
+
+(new Router())->reroute();

From 49165e91d97334daa76307373f8c754eabd8f03a Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 9 Jun 2015 14:02:29 -0400
Subject: [PATCH 04/39] SAAS-854 - celery-based SoundCloud backend uploader

---
 airtime_mvc/application/Bootstrap.php         |   2 +-
 airtime_mvc/application/models/RabbitMq.php   |  35 +-
 .../services/SoundCloudService.php            |  15 +-
 composer.json                                 |   3 +-
 composer.lock                                 | 159 ++++++++-
 .../bin/soundcloud_uploader                   |   8 +
 .../install/conf/soundcloud_uploader          |  25 ++
 .../install/upstart/soundcloud_uploader       | 333 ++++++++++++++++++
 python_apps/soundcloud_uploader/setup.py      |  35 ++
 .../soundcloud_uploader/__init__.py           |   3 +
 .../soundcloud_uploader/celeryconfig.py       |  42 +++
 .../soundcloud_uploader/uploader.py           |  24 ++
 12 files changed, 672 insertions(+), 12 deletions(-)
 create mode 100644 python_apps/soundcloud_uploader/bin/soundcloud_uploader
 create mode 100644 python_apps/soundcloud_uploader/install/conf/soundcloud_uploader
 create mode 100644 python_apps/soundcloud_uploader/install/upstart/soundcloud_uploader
 create mode 100644 python_apps/soundcloud_uploader/setup.py
 create mode 100644 python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py
 create mode 100644 python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py
 create mode 100644 python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 33ad21d84..7baa3f5ac 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -27,7 +27,7 @@ require_once "ProvisioningHelper.php";
 require_once "GoogleAnalytics.php";
 require_once "Timezone.php";
 require_once "Auth.php";
-require_once __DIR__ . '/services/SoundCloudService.php';
+require_once __DIR__.'/services/SoundCloudService.php';
 require_once __DIR__.'/forms/helpers/ValidationTypes.php';
 require_once __DIR__.'/forms/helpers/CustomDecorators.php';
 require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php';
diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php
index fa05f3986..c9492436b 100644
--- a/airtime_mvc/application/models/RabbitMq.php
+++ b/airtime_mvc/application/models/RabbitMq.php
@@ -1,10 +1,13 @@
 <?php
 require_once 'php-amqplib/amqp.inc';
+require_once 'massivescale/celery-php/celery.php';
 
 class Application_Model_RabbitMq
 {
     public static $doPush = false;
 
+    const CELERY_TIMEOUT = 10;
+
     /**
      * Sets a flag to push the schedule at the end of the request.
      */
@@ -42,6 +45,32 @@ class Application_Model_RabbitMq
         $conn->close();
     }
 
+    public static function sendCeleryMessage($task, $exchange, $data) {
+        $CC_CONFIG = Config::getConfig();
+
+        $c = new Celery($CC_CONFIG["rabbitmq"]["host"],
+                        $CC_CONFIG["rabbitmq"]["user"],
+                        $CC_CONFIG["rabbitmq"]["password"],
+                        $CC_CONFIG["rabbitmq"]["vhost"],
+                        $exchange=$exchange);
+        $result = $c->PostTask($task, $data);
+
+        $timeout = 0;
+        while(!$result->isReady()) {
+            sleep(1);
+            if($timeout++ >= self::CELERY_TIMEOUT) {
+                break;
+            }
+        }
+
+        if($result->isSuccess()) {
+            Logging::info($result);
+            return $result->getResult();
+        } else {
+            throw new CeleryTimeoutException("Celery task $task timed out!");
+        }
+    }
+
     public static function SendMessageToPypo($event_type, $md)
     {
         $md["event_type"] = $event_type;
@@ -146,5 +175,9 @@ class Application_Model_RabbitMq
     
     public static function SendMessageToHaproxyConfigDaemon($md){
         //XXX: This function has been deprecated and is no longer needed
-    }        
+    }
+
+    public static function uploadToSoundCloud($data) {
+        return self::sendCeleryMessage("upload", "soundcloud-uploads", $data);
+    }
 }
diff --git a/airtime_mvc/application/services/SoundCloudService.php b/airtime_mvc/application/services/SoundCloudService.php
index 61895ca59..dbfe0c83c 100644
--- a/airtime_mvc/application/services/SoundCloudService.php
+++ b/airtime_mvc/application/services/SoundCloudService.php
@@ -44,7 +44,6 @@ class SoundcloudService extends ThirdPartyService {
         }
     }
 
-    // TODO: upload functionality will be moved to python, this is just for testing
     /**
      * Upload the file with the given identifier to SoundCloud
      *
@@ -55,8 +54,13 @@ class SoundcloudService extends ThirdPartyService {
      */
     public function upload($fileId) {
         $file = Application_Model_StoredFile::RecallById($fileId);
+        $data = array(
+            'track_data' => $this->_buildTrackArray($file),
+            'token' => $this->_client->getAccessToken(),
+            'file_path' => $file->getFilePaths()[0]
+        );
         try {
-            $track = json_decode($this->_client->post('tracks', $this->_buildTrackArray($file)));
+            $track = json_decode(Application_Model_RabbitMq::uploadToSoundCloud($data));
             parent::_createTrackReference($fileId, $track);
         } catch(Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
             Logging::info("Invalid request: " . $e->getMessage());
@@ -74,15 +78,12 @@ class SoundcloudService extends ThirdPartyService {
      */
     private function _buildTrackArray($file) {
         $trackArray = array(
-            'track[title]' => $file->getName(),
-            // TODO: verify that S3 uploads work
-            'track[asset_data]' => '@'.$file->getFilePaths()[0]
+            'title' => $file->getName(),
         );
-
         foreach($this->_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
             $val = Application_Model_Preference::$func();
             if (!empty($val)) {
-                $trackArray["track[$param]"] = $val;
+                $trackArray[$param] = $val;
             }
         }
 
diff --git a/composer.json b/composer.json
index 03adc292a..4b4827efd 100644
--- a/composer.json
+++ b/composer.json
@@ -3,6 +3,7 @@
         "propel/propel1": "1.7.0-stable",
         "aws/aws-sdk-php": "2.7.9",
         "raven/raven": "0.8.x-dev",
-        "ise/php-soundcloud": "3.0.1"
+        "ise/php-soundcloud": "3.0.1",
+        "massivescale/celery-php": "2.0.*@dev"
     }
 }
diff --git a/composer.lock b/composer.lock
index c819c7f39..528dcf222 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "e731a5b93a15b54d4c22e26f33dc1aaa",
+    "hash": "8a3acbb09ff4547ccbbeb6ec2ee893f8",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
@@ -213,6 +213,59 @@
             ],
             "time": "2014-02-03 15:49:00"
         },
+        {
+            "name": "massivescale/celery-php",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/gjedeer/celery-php.git",
+                "reference": "609720abe0683ffd3c5d2dfc8a898a79b687f599"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/gjedeer/celery-php/zipball/609720abe0683ffd3c5d2dfc8a898a79b687f599",
+                "reference": "609720abe0683ffd3c5d2dfc8a898a79b687f599",
+                "shasum": ""
+            },
+            "require": {
+                "predis/predis": "0.8.5",
+                "videlalvaro/php-amqplib": ">=2.4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "celery.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "GDR!",
+                    "email": "info@massivescale.net",
+                    "homepage": "http://massivescale.net/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "PHP client for Celery task queue",
+            "homepage": "https://github.com/gjedeer/celery-php/",
+            "keywords": [
+                "AMQP",
+                "celery",
+                "cron",
+                "python",
+                "queue",
+                "task"
+            ],
+            "time": "2015-04-17 10:58:54"
+        },
         {
             "name": "phing/phing",
             "version": "2.9.1",
@@ -302,6 +355,53 @@
             ],
             "time": "2014-12-03 09:18:46"
         },
+        {
+            "name": "predis/predis",
+            "version": "v0.8.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nrk/predis.git",
+                "reference": "5f2eea628eb465d866ad2771927d83769c8f956c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nrk/predis/zipball/5f2eea628eb465d866ad2771927d83769c8f956c",
+                "reference": "5f2eea628eb465d866ad2771927d83769c8f956c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "suggest": {
+                "ext-curl": "Allows access to Webdis when paired with phpiredis",
+                "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Predis": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniele Alessandri",
+                    "email": "suppakilla@gmail.com",
+                    "homepage": "http://clorophilla.net"
+                }
+            ],
+            "description": "Flexible and feature-complete PHP client library for Redis",
+            "homepage": "http://github.com/nrk/predis",
+            "keywords": [
+                "nosql",
+                "predis",
+                "redis"
+            ],
+            "time": "2014-01-16 14:10:29"
+        },
         {
             "name": "propel/propel1",
             "version": "1.7.0",
@@ -474,13 +574,68 @@
             "description": "Symfony EventDispatcher Component",
             "homepage": "http://symfony.com",
             "time": "2015-01-05 14:28:40"
+        },
+        {
+            "name": "videlalvaro/php-amqplib",
+            "version": "v2.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/videlalvaro/php-amqplib.git",
+                "reference": "7989ddce7c4aa6038483d8999b12d3b1f7c81ff9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/videlalvaro/php-amqplib/zipball/7989ddce7c4aa6038483d8999b12d3b1f7c81ff9",
+                "reference": "7989ddce7c4aa6038483d8999b12d3b1f7c81ff9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-bcmath": "*",
+                "ext-mbstring": "*",
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "3.7.*"
+            },
+            "suggest": {
+                "ext-sockets": "Use AMQPSocketConnection"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "PhpAmqpLib": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1"
+            ],
+            "authors": [
+                {
+                    "name": "Alvaro Videla"
+                }
+            ],
+            "description": "This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.",
+            "homepage": "https://github.com/videlalvaro/php-amqplib/",
+            "keywords": [
+                "message",
+                "queue",
+                "rabbitmq"
+            ],
+            "time": "2015-02-18 18:16:04"
         }
     ],
     "packages-dev": [],
     "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
-        "raven/raven": 20
+        "raven/raven": 20,
+        "massivescale/celery-php": 20
     },
     "prefer-stable": false,
     "prefer-lowest": false,
diff --git a/python_apps/soundcloud_uploader/bin/soundcloud_uploader b/python_apps/soundcloud_uploader/bin/soundcloud_uploader
new file mode 100644
index 000000000..7ecdd2b88
--- /dev/null
+++ b/python_apps/soundcloud_uploader/bin/soundcloud_uploader
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+
+if [ "$(id -u)" != "0" ]; then
+    echo "Please run as root user."
+    exit 1
+fi
+
+service soundcloud_uploader restart
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/install/conf/soundcloud_uploader b/python_apps/soundcloud_uploader/install/conf/soundcloud_uploader
new file mode 100644
index 000000000..4276e402f
--- /dev/null
+++ b/python_apps/soundcloud_uploader/install/conf/soundcloud_uploader
@@ -0,0 +1,25 @@
+# Names of nodes to start
+CELERYD_NODES="soundcloud_uploader"
+
+# Absolute or relative path to the 'celery' command:
+CELERY_BIN="/usr/local/bin/celery"
+
+# App instance to use
+CELERY_APP="soundcloud_uploader.uploader:celery"
+
+# Extra command-line arguments to the worker
+CELERYD_OPTS="--time-limit=300 --concurrency=8 --config=celeryconfig"
+
+# %N will be replaced with the first part of the nodename.
+CELERYD_LOG_FILE="/var/log/airtime/%N.log"
+CELERYD_PID_FILE="/var/run/celery/%N.pid"
+
+# Workers should run as an unprivileged user.
+#   You need to create this user manually (or you can choose
+#   a user/group combination that already exists, e.g. nobody).
+CELERYD_USER="celery"
+CELERYD_GROUP="celery"
+
+# If enabled pid and log directories will be created if missing,
+# and owned by the userid/group configured.
+CELERY_CREATE_DIRS=1
diff --git a/python_apps/soundcloud_uploader/install/upstart/soundcloud_uploader b/python_apps/soundcloud_uploader/install/upstart/soundcloud_uploader
new file mode 100644
index 000000000..34e990ab8
--- /dev/null
+++ b/python_apps/soundcloud_uploader/install/upstart/soundcloud_uploader
@@ -0,0 +1,333 @@
+#!/bin/sh -e
+# ============================================
+#  celeryd - Starts the Celery worker daemon.
+# ============================================
+#
+# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
+# :Configuration file: /etc/default/celeryd
+#
+# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
+
+
+### BEGIN INIT INFO
+# Provides:          celeryd
+# Required-Start:    $network $local_fs $remote_fs
+# Required-Stop:     $network $local_fs $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: celery task worker daemon
+### END INIT INFO
+#
+#
+# To implement separate init scripts, copy this script and give it a different
+# name:
+# I.e., if my new application, "little-worker" needs an init, I
+# should just use:
+#
+#   cp /etc/init.d/celeryd /etc/init.d/little-worker
+#
+# You can then configure this by manipulating /etc/default/little-worker.
+#
+VERSION=10.1
+echo "celery init v${VERSION}."
+if [ $(id -u) -ne 0 ]; then
+    echo "Error: This program can only be used by the root user."
+    echo "       Unprivileged users must use the 'celery multi' utility, "
+    echo "       or 'celery worker --detach'."
+    exit 1
+fi
+
+
+# Can be a runlevel symlink (e.g. S02celeryd)
+if [ -L "$0" ]; then
+    SCRIPT_FILE=$(readlink "$0")
+else
+    SCRIPT_FILE="$0"
+fi
+SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
+
+DEFAULT_USER="celery"
+DEFAULT_PID_FILE="/var/run/celery/%n.pid"
+DEFAULT_LOG_FILE="/var/log/celery/%n.log"
+DEFAULT_LOG_LEVEL="INFO"
+DEFAULT_NODES="celery"
+DEFAULT_CELERYD="-m celery worker --detach"
+
+CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
+# Make sure executable configuration script is owned by root
+_config_sanity() {
+    local path="$1"
+    local owner=$(ls -ld "$path" | awk '{print $3}')
+    local iwgrp=$(ls -ld "$path" | cut -b 6)
+    local iwoth=$(ls -ld "$path" | cut -b 9)
+    if [ "$(id -u $owner)" != "0" ]; then
+        echo "Error: Config script '$path' must be owned by root!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with mailicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change ownership of the script:"
+        echo "    $ sudo chown root '$path'"
+        exit 1
+    fi
+    if [ "$iwoth" != "-" ]; then  # S_IWOTH
+        echo "Error: Config script '$path' cannot be writable by others!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
+        echo "Error: Config script '$path' cannot be writable by group!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+}
+if [ -f "$CELERY_DEFAULTS" ]; then
+    _config_sanity "$CELERY_DEFAULTS"
+    echo "Using config script: $CELERY_DEFAULTS"
+    . "$CELERY_DEFAULTS"
+fi
+# Sets --app argument for CELERY_BIN
+CELERY_APP_ARG=""
+if [ ! -z "$CELERY_APP" ]; then
+    CELERY_APP_ARG="--app=$CELERY_APP"
+fi
+CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
+# Set CELERY_CREATE_DIRS to always create log/pid dirs.
+CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
+CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
+CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
+if [ -z "$CELERYD_PID_FILE" ]; then
+    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
+    CELERY_CREATE_RUNDIR=1
+fi
+if [ -z "$CELERYD_LOG_FILE" ]; then
+    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
+    CELERY_CREATE_LOGDIR=1
+fi
+CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
+CELERY_BIN=${CELERY_BIN:-"celery"}
+CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
+CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
+export CELERY_LOADER
+if [ -n "$2" ]; then
+    CELERYD_OPTS="$CELERYD_OPTS $2"
+fi
+CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
+CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
+# Extra start-stop-daemon options, like user/group.
+if [ -n "$CELERYD_CHDIR" ]; then
+    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
+fi
+check_dev_null() {
+    if [ ! -c /dev/null ]; then
+        echo "/dev/null is not a character device!"
+        exit 75  # EX_TEMPFAIL
+    fi
+}
+maybe_die() {
+    if [ $? -ne 0 ]; then
+        echo "Exiting: $* (errno $?)"
+        exit 77  # EX_NOPERM
+    fi
+}
+create_default_dir() {
+    if [ ! -d "$1" ]; then
+        echo "- Creating default directory: '$1'"
+        mkdir -p "$1"
+        maybe_die "Couldn't create directory $1"
+        echo "- Changing permissions of '$1' to 02755"
+        chmod 02755 "$1"
+        maybe_die "Couldn't change permissions for $1"
+        if [ -n "$CELERYD_USER" ]; then
+            echo "- Changing owner of '$1' to '$CELERYD_USER'"
+            chown "$CELERYD_USER" "$1"
+            maybe_die "Couldn't change owner of $1"
+        fi
+        if [ -n "$CELERYD_GROUP" ]; then
+            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
+            chgrp "$CELERYD_GROUP" "$1"
+            maybe_die "Couldn't change group of $1"
+        fi
+    fi
+}
+check_paths() {
+    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_LOG_DIR"
+    fi
+    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_PID_DIR"
+    fi
+}
+create_paths() {
+    create_default_dir "$CELERYD_LOG_DIR"
+    create_default_dir "$CELERYD_PID_DIR"
+}
+export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
+_get_pidfiles () {
+    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
+    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
+}
+_get_pids() {
+    found_pids=0
+    my_exitcode=0
+    for pidfile in $(_get_pidfiles); do
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+            my_exitcode=1
+        else
+            found_pids=1
+            echo "$pid"
+        fi
+    if [ $found_pids -eq 0 ]; then
+        echo "${SCRIPT_NAME}: All nodes down"
+        exit $my_exitcode
+    fi
+    done
+}
+_chuid () {
+    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
+}
+start_workers () {
+    if [ ! -z "$CELERYD_ULIMIT" ]; then
+        ulimit $CELERYD_ULIMIT
+    fi
+    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
+                 --pidfile="$CELERYD_PID_FILE"      \
+                 --logfile="$CELERYD_LOG_FILE"      \
+                 --loglevel="$CELERYD_LOG_LEVEL"    \
+                 $CELERY_APP_ARG                    \
+                 $CELERYD_OPTS
+}
+dryrun () {
+    (C_FAKEFORK=1 start_workers --verbose)
+}
+stop_workers () {
+    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers () {
+    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
+                   --pidfile="$CELERYD_PID_FILE"    \
+                   --logfile="$CELERYD_LOG_FILE"    \
+                   --loglevel="$CELERYD_LOG_LEVEL"  \
+                   $CELERY_APP_ARG                  \
+                   $CELERYD_OPTS
+}
+kill_workers() {
+    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers_graceful () {
+    echo "WARNING: Use with caution in production"
+    echo "The workers will attempt to restart, but they may not be able to."
+    local worker_pids=
+    worker_pids=`_get_pids`
+    [ "$one_failed" ] && exit 1
+    for worker_pid in $worker_pids; do
+        local failed=
+        kill -HUP $worker_pid 2> /dev/null || failed=true
+        if [ "$failed" ]; then
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
+            one_failed=true
+        else
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+check_status () {
+    my_exitcode=0
+    found_pids=0
+    local one_failed=
+    for pidfile in $(_get_pidfiles); do
+        if [ ! -r $pidfile ]; then
+            echo "${SCRIPT_NAME} down: no pidfiles found"
+            one_failed=true
+            break
+        fi
+        local node=`basename "$pidfile" .pid`
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+        else
+            local failed=
+            kill -0 $pid 2> /dev/null || failed=true
+            if [ "$failed" ]; then
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
+                one_failed=true
+            else
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
+            fi
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+case "$1" in
+    start)
+        check_dev_null
+        check_paths
+        start_workers
+    ;;
+    stop)
+        check_dev_null
+        check_paths
+        stop_workers
+    ;;
+    reload|force-reload)
+        echo "Use restart"
+    ;;
+    status)
+        check_status
+    ;;
+    restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    graceful)
+        check_dev_null
+        restart_workers_graceful
+    ;;
+    kill)
+        check_dev_null
+        kill_workers
+    ;;
+    dryrun)
+        check_dev_null
+        dryrun
+    ;;
+    try-restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    create-paths)
+        check_dev_null
+        create_paths
+    ;;
+    check-paths)
+        check_dev_null
+        check_paths
+    ;;
+    *)
+        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
+        exit 64  # EX_USAGE
+    ;;
+esac
+exit 0
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/setup.py b/python_apps/soundcloud_uploader/setup.py
new file mode 100644
index 000000000..5625f7c89
--- /dev/null
+++ b/python_apps/soundcloud_uploader/setup.py
@@ -0,0 +1,35 @@
+from setuptools import setup
+from subprocess import call
+import os
+
+data_files = [('/etc/default', ['install/conf/soundcloud_uploader']),
+              ('/etc/init.d', ['install/upstart/soundcloud_uploader'])]
+print data_files
+
+setup(name='soundcloud_uploader',
+      version='0.1',
+      description='Celery SoundCloud upload worker',
+      url='http://github.com/sourcefabric/Airtime',
+      author='Sourcefabric',
+      author_email='duncan.sommerville@sourcefabric.org',
+      license='MIT',
+      packages=['soundcloud_uploader'],
+      scripts=['bin/soundcloud_uploader'],
+      install_requires=[
+          'soundcloud',
+          'celery',
+          'kombu'
+      ],
+      zip_safe=False,
+      data_files=data_files)
+
+if data_files:
+    print "Reloading initctl configuration"
+    call(['initctl', 'reload-configuration'])
+    # Make /etc/init.d file executable and set proper
+    # permissions for the defaults config file
+    os.chmod('/etc/init.d/soundcloud_uploader', 0755)
+    os.chmod('/etc/default/soundcloud_uploader', 0640)
+    print "Setting uploader to start on boot"
+    call(['update-rc.d', 'soundcloud_uploader', 'defaults'])
+    print "Run \"sudo service soundcloud_uploader restart\" now."
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py b/python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py
new file mode 100644
index 000000000..d7484a82e
--- /dev/null
+++ b/python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py
@@ -0,0 +1,3 @@
+import os
+# Make the celeryconfig module visible to celery
+os.environ['CELERY_CONFIG_MODULE'] = 'soundcloud_uploader.celeryconfig'
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py b/python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py
new file mode 100644
index 000000000..08e67821f
--- /dev/null
+++ b/python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py
@@ -0,0 +1,42 @@
+from configobj import ConfigObj
+from kombu import Exchange, Queue
+
+# Get the broker string from airtime.conf
+DEFAULT_RMQ_CONFIG_PATH = '/etc/airtime/airtime.conf'
+RMQ_CONFIG_SECTION = "rabbitmq"
+
+
+def parse_rmq_config(rmq_config):
+    return {
+        'host'     : rmq_config[RMQ_CONFIG_SECTION]['host'],
+        'port'     : rmq_config[RMQ_CONFIG_SECTION]['port'],
+        'username' : rmq_config[RMQ_CONFIG_SECTION]['user'],
+        'password' : rmq_config[RMQ_CONFIG_SECTION]['password'],
+        'vhost'    : rmq_config[RMQ_CONFIG_SECTION]['vhost']
+    }
+
+
+def get_rmq_broker():
+    rmq_config = ConfigObj(DEFAULT_RMQ_CONFIG_PATH)
+    rmq_settings = parse_rmq_config(rmq_config)
+    return 'amqp://{username}:{password}@{host}:{port}/{vhost}'.format(**rmq_settings)
+
+# Celery amqp settings
+BROKER_URL = get_rmq_broker()
+CELERY_RESULT_BACKEND = 'amqp'     # Use RabbitMQ as the celery backend
+# CELERY_RESULT_EXCHANGE = 'upload-results'
+CELERY_RESULT_PERSISTENT = True    # Persist through a broker restart
+CELERY_TASK_RESULT_EXPIRES = None  # Don't expire tasks
+CELERY_TRACK_STARTED = False
+CELERY_QUEUES = (
+    Queue('soundcloud-uploads', Exchange('soundcloud-uploads'), routing_key='celery'),
+)
+CELERY_DEFAULT_QUEUE = 'soundcloud-uploads'
+CELERY_DEFAULT_EXCHANGE_TYPE = 'topic'
+
+# Celery task settings
+CELERY_TASK_SERIALIZER = 'json'
+CELERY_RESULT_SERIALIZER = 'json'
+CELERY_ACCEPT_CONTENT = ['json']
+CELERY_TIMEZONE = 'Europe/Berlin'
+CELERY_ENABLE_UTC = True
diff --git a/python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py b/python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py
new file mode 100644
index 000000000..4fc24c21b
--- /dev/null
+++ b/python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py
@@ -0,0 +1,24 @@
+import os
+import json
+import urllib2
+import soundcloud
+from celery import Celery
+from celery.utils.log import get_task_logger
+
+celery = Celery('uploader')
+logger = get_task_logger(__name__)
+
+
+@celery.task(queue='soundcloud-uploads', name='upload')
+def upload(track_data, token, file_path):
+    client = soundcloud.Client(access_token=token)
+    # Open the file with urllib2 if it's a cloud file
+    track_data['asset_data'] = open(file_path, 'rb') if os.path.isfile(file_path) else urllib2.urlopen(file_path)
+    try:
+        logger.info('Uploading track: {0}'.format(track_data))
+        track = client.post('/tracks', track=track_data)
+    except Exception as e:
+        logger.info('Error uploading track {name}: {0}'.format(e.message, **track_data))
+        raise e
+    track_data['asset_data'].close()
+    return json.dumps(track.fields())

From 626489bb3bd43ade681ea08e6e84889152a51d2a Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 10 Jun 2015 15:04:49 -0400
Subject: [PATCH 05/39] SAAS-853 - Celery backend for SoundCloud uploads

---
 airtime_mvc/application/Bootstrap.php         |   2 +-
 airtime_mvc/application/configs/conf.php      |   1 +
 .../controllers/LibraryController.php         |   1 -
 .../controllers/SoundcloudController.php      |  50 +---
 .../controllers/ThirdPartyController.php      |  56 ++++-
 .../controllers/UpgradeController.php         |   4 +-
 .../upgrade_sql/airtime_2.5.13/upgrade.sql    |  11 +-
 airtime_mvc/application/models/RabbitMq.php   |  95 ++++++--
 .../map/ThirdPartyTrackReferencesTableMap.php |   7 +-
 .../om/BaseThirdPartyTrackReferences.php      | 224 ++++++++++++++++--
 .../om/BaseThirdPartyTrackReferencesPeer.php  |  43 ++--
 .../om/BaseThirdPartyTrackReferencesQuery.php | 130 ++++++++--
 ...CloudService.php => SoundcloudService.php} |  81 ++++---
 .../services/ThirdPartyService.php            | 164 ++++++++++++-
 airtime_mvc/build/schema.xml                  |  14 +-
 airtime_mvc/build/sql/schema.sql              |  11 +-
 .../public/js/airtime/common/common.js        |   6 +
 .../airtime-celery/airtime-celery/__init__.py |   3 +
 .../airtime-celery}/celeryconfig.py           |  17 +-
 .../airtime-celery/airtime-celery/uploader.py |  24 ++
 .../install/conf/airtime-celery}              |   4 +-
 .../install/upstart/airtime-celery}           |   0
 python_apps/airtime-celery/setup.py           |  45 ++++
 .../bin/soundcloud_uploader                   |   8 -
 python_apps/soundcloud_uploader/setup.py      |  35 ---
 .../soundcloud_uploader/__init__.py           |   3 -
 .../soundcloud_uploader/uploader.py           |  24 --
 27 files changed, 813 insertions(+), 250 deletions(-)
 rename airtime_mvc/application/services/{SoundCloudService.php => SoundcloudService.php} (71%)
 create mode 100644 python_apps/airtime-celery/airtime-celery/__init__.py
 rename python_apps/{soundcloud_uploader/soundcloud_uploader => airtime-celery/airtime-celery}/celeryconfig.py (69%)
 create mode 100644 python_apps/airtime-celery/airtime-celery/uploader.py
 rename python_apps/{soundcloud_uploader/install/conf/soundcloud_uploader => airtime-celery/install/conf/airtime-celery} (89%)
 rename python_apps/{soundcloud_uploader/install/upstart/soundcloud_uploader => airtime-celery/install/upstart/airtime-celery} (100%)
 create mode 100644 python_apps/airtime-celery/setup.py
 delete mode 100644 python_apps/soundcloud_uploader/bin/soundcloud_uploader
 delete mode 100644 python_apps/soundcloud_uploader/setup.py
 delete mode 100644 python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py
 delete mode 100644 python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 7baa3f5ac..7580eef69 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -27,7 +27,7 @@ require_once "ProvisioningHelper.php";
 require_once "GoogleAnalytics.php";
 require_once "Timezone.php";
 require_once "Auth.php";
-require_once __DIR__.'/services/SoundCloudService.php';
+require_once __DIR__.'/services/SoundcloudService.php';
 require_once __DIR__.'/forms/helpers/ValidationTypes.php';
 require_once __DIR__.'/forms/helpers/CustomDecorators.php';
 require_once __DIR__.'/controllers/plugins/RabbitMqPlugin.php';
diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php
index afc1d5f07..80199b38c 100644
--- a/airtime_mvc/application/configs/conf.php
+++ b/airtime_mvc/application/configs/conf.php
@@ -37,6 +37,7 @@ class Config {
         $CC_CONFIG['baseDir'] = $values['general']['base_dir'];
         $CC_CONFIG['baseUrl'] = $values['general']['base_url'];
         $CC_CONFIG['basePort'] = $values['general']['base_port'];
+        $CC_CONFIG['stationId'] = $values['general']['station_id'];
         $CC_CONFIG['phpDir'] = $values['general']['airtime_dir'];
         if (isset($values['general']['dev_env'])) {
             $CC_CONFIG['dev_env'] = $values['general']['dev_env'];
diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index 07bbc109b..91816901f 100644
--- a/airtime_mvc/application/controllers/LibraryController.php
+++ b/airtime_mvc/application/controllers/LibraryController.php
@@ -283,7 +283,6 @@ class LibraryController extends Zend_Controller_Action
                 $text = _("Upload to SoundCloud");
             }
 
-            // TODO: reimplement how this works
             $menu["soundcloud"]["items"]["upload"] = array("name" => $text, "icon" => "soundcloud", "url" => $baseUrl."soundcloud/upload/id/{$id}");
         }
 
diff --git a/airtime_mvc/application/controllers/SoundcloudController.php b/airtime_mvc/application/controllers/SoundcloudController.php
index b265f5740..c45597b87 100644
--- a/airtime_mvc/application/controllers/SoundcloudController.php
+++ b/airtime_mvc/application/controllers/SoundcloudController.php
@@ -8,32 +8,19 @@ class SoundcloudController extends ThirdPartyController {
     /**
      * @var SoundcloudService
      */
-    private $_soundcloudService;
+    protected $_service;
+
+    /**
+     * @var string Application_Model_Preference service request token accessor function name
+     */
+    protected $_SERVICE_TOKEN_ACCESSOR = 'setSoundCloudRequestToken';
 
     /**
      * Set up SoundCloud access variables.
      */
     public function init() {
         parent::init();
-        $this->_soundcloudService = new SoundcloudService();
-    }
-
-    /**
-     * Send user to SoundCloud to authorize before being redirected
-     */
-    public function authorizeAction() {
-        $auth_url = $this->_soundcloudService->getAuthorizeUrl();
-        header('Location: ' . $auth_url);
-    }
-
-    /**
-     * Called when user successfully completes SoundCloud authorization.
-     * Store the returned request token for future requests.
-     */
-    public function redirectAction() {
-        $code = $_GET['code'];
-        $this->_soundcloudService->requestNewAccessToken($code);
-        header('Location: ' . $this->_baseUrl . 'Preference'); // Redirect back to the Preference page
+        $this->_service = new SoundcloudService();
     }
 
     /**
@@ -43,36 +30,17 @@ class SoundcloudController extends ThirdPartyController {
         $request = $this->getRequest();
         $id = $request->getParam('id');
         try {
-            $soundcloudLink = $this->_soundcloudService->getLinkToFile($id);
+            $soundcloudLink = $this->_service->getLinkToFile($id);
             header('Location: ' . $soundcloudLink);
         } catch (Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
             // If we end up here it means the track was removed from SoundCloud
             // or the foreign id in our database is incorrect, so we should just
             // get rid of the database record
             Logging::warn("Error retrieving track data from SoundCloud: " . $e->getMessage());
-            $this->_soundcloudService->removeTrackReference($id);
+            $this->_service->removeTrackReference($id);
             // Redirect to a 404 so the user knows something went wrong
             header('Location: ' . $this->_baseUrl . 'error/error-404'); // Redirect back to the Preference page
         }
     }
 
-    /**
-     * Upload the file with the given id to SoundCloud.
-     *
-     * @throws Zend_Controller_Response_Exception thrown if upload fails for any reason
-     */
-    public function uploadAction() {
-        $request = $this->getRequest();
-        $id = $request->getParam('id');
-        $this->_soundcloudService->upload($id);
-    }
-
-    /**
-     * Clear the previously saved request token from the preferences.
-     */
-    public function deauthorizeAction() {
-        Application_Model_Preference::setSoundCloudRequestToken("");
-        header('Location: ' . $this->_baseUrl . 'Preference'); // Redirect back to the Preference page
-    }
-
 }
diff --git a/airtime_mvc/application/controllers/ThirdPartyController.php b/airtime_mvc/application/controllers/ThirdPartyController.php
index 2fec86161..139d0c014 100644
--- a/airtime_mvc/application/controllers/ThirdPartyController.php
+++ b/airtime_mvc/application/controllers/ThirdPartyController.php
@@ -10,6 +10,16 @@ abstract class ThirdPartyController extends Zend_Controller_Action {
      */
     protected $_baseUrl;
 
+    /**
+     * @var ThirdPartyService third party service object
+     */
+    protected $_service;
+
+    /**
+     * @var string Application_Model_Preference service request token accessor function name
+     */
+    protected $_SERVICE_TOKEN_ACCESSOR;
+
     /**
      * Disable controller rendering and initialize
      */
@@ -17,8 +27,8 @@ abstract class ThirdPartyController extends Zend_Controller_Action {
         $CC_CONFIG = Config::getConfig();
         $this->_baseUrl = 'http://' . $CC_CONFIG['baseUrl'] . ":" . $CC_CONFIG['basePort'] . "/";
 
-        $this->view->layout()->disableLayout(); // Don't inject the standard Now Playing header.
-        $this->_helper->viewRenderer->setNoRender(true); // Don't use (phtml) templates
+        $this->view->layout()->disableLayout();  // Don't inject the standard Now Playing header.
+        $this->_helper->viewRenderer->setNoRender(true);  // Don't use (phtml) templates
     }
 
     /**
@@ -26,30 +36,56 @@ abstract class ThirdPartyController extends Zend_Controller_Action {
      *
      * @return void
      */
-    abstract function authorizeAction();
+    public function authorizeAction() {
+        $auth_url = $this->_service->getAuthorizeUrl();
+        header('Location: ' . $auth_url);
+    }
 
     /**
-     * Called when user successfully completes third-party authorization.
-     * Store the returned request token for future requests.
+     * Called when user successfully completes third-party authorization
+     * Store the returned request token for future requests
      *
      * @return void
      */
-    abstract function redirectAction();
+    public function redirectAction() {
+        $code = $_GET['code'];
+        $this->_service->requestNewAccessToken($code);
+        header('Location: ' . $this->_baseUrl . 'Preference');  // Redirect back to the Preference page
+    }
 
     /**
-     * Upload the file with the given id to a third-party service.
+     * Upload the file with the given id to a third-party service
      *
      * @return void
      *
      * @throws Zend_Controller_Response_Exception thrown if upload fails for any reason
      */
-    abstract function uploadAction();
+    public function uploadAction() {
+        $request = $this->getRequest();
+        $id = $request->getParam('id');
+        $this->_service->upload($id);
+    }
 
     /**
-     * Clear the previously saved request token from the preferences.
+     * Clear the previously saved request token from the preferences
      *
      * @return void
      */
-    abstract function deauthorizeAction();
+    public function deauthorizeAction() {
+        Application_Model_Preference::$this->_SERVICE_TOKEN_ACCESSOR("");
+        header('Location: ' . $this->_baseUrl . 'Preference');  // Redirect back to the Preference page
+    }
+
+    /**
+     * Poll the task queue for completed tasks associated with this service
+     * Optionally accepts a specific task name as a parameter
+     *
+     * @return void
+     */
+    public function pollBrokerTaskQueueAction() {
+        $request = $this->getRequest();
+        $taskName = $request->getParam('task');
+        $this->_service->pollBrokerTaskQueue($taskName);
+    }
 
 }
\ No newline at end of file
diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php
index d3cee649b..4c04da699 100644
--- a/airtime_mvc/application/controllers/UpgradeController.php
+++ b/airtime_mvc/application/controllers/UpgradeController.php
@@ -13,9 +13,9 @@ class UpgradeController extends Zend_Controller_Action
             return;
         }
 
-        // Get all upgrades dynamically so we don't have to add them explicitly each time
+        // Get all upgrades dynamically (in declaration order!) so we don't have to add them explicitly each time
+        // TODO: explicitly sort classnames by ascending version suffix for safety
         $upgraders = getUpgrades();
-        Logging::info($upgraders);
 
         $didWePerformAnUpgrade = false;
         try 
diff --git a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
index e2d051bff..15a6432d7 100644
--- a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
+++ b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
@@ -1,11 +1,16 @@
 CREATE TABLE IF NOT EXISTS "third_party_track_references"
 (
     "id" serial NOT NULL,
-    "service" VARCHAR(512) NOT NULL,
-    "foreign_id" INTEGER NOT NULL,
+    "service" VARCHAR(256) NOT NULL,
+    "foreign_id" VARCHAR(256),
+    "broker_task_id" VARCHAR(256),
+    "broker_task_name" VARCHAR(256),
+    "broker_task_dispatch_time" TIMESTAMP,
     "file_id" INTEGER NOT NULL,
     "status" VARCHAR(256) NOT NULL,
-    PRIMARY KEY ("id")
+    PRIMARY KEY ("id"),
+    CONSTRAINT "broker_task_id_unique" UNIQUE ("broker_task_id"),
+    CONSTRAINT "foreign_id_unique" UNIQUE ("foreign_id")
 );
 
 ALTER TABLE "third_party_track_references" ADD CONSTRAINT "track_reference_fkey"
diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php
index c9492436b..da949cd88 100644
--- a/airtime_mvc/application/models/RabbitMq.php
+++ b/airtime_mvc/application/models/RabbitMq.php
@@ -6,7 +6,15 @@ class Application_Model_RabbitMq
 {
     public static $doPush = false;
 
-    const CELERY_TIMEOUT = 10;
+    /**
+     * @var int milliseconds (for compatibility with celery) until we consider a message to have timed out
+     */
+    public static $_CELERY_MESSAGE_TIMEOUT = 300000;  // 5 minutes
+
+    /**
+     * @var string exchange for celery task results
+     */
+    public static $_CELERY_RESULTS_EXCHANGE = 'airtime-results';
 
     /**
      * Sets a flag to push the schedule at the end of the request.
@@ -45,30 +53,72 @@ class Application_Model_RabbitMq
         $conn->close();
     }
 
+    /**
+     * Connect to the Celery daemon via amqp
+     *
+     * @param $config   array  the airtime configuration array
+     * @param $exchange string the amqp exchange name
+     * @param $queue    string the amqp queue name
+     *
+     * @return Celery the Celery connection object
+     *
+     * @throws Exception when a connection error occurs
+     */
+    private static function _setupCeleryExchange($config, $exchange, $queue) {
+        return new Celery($config["rabbitmq"]["host"],
+                          $config["rabbitmq"]["user"],
+                          $config["rabbitmq"]["password"],
+                          $config["rabbitmq"]["vhost"],
+                          $exchange,                        // Exchange name
+                          $queue,                           // Binding/queue
+                          $config["rabbitmq"]["port"],
+                          false,                            // Connector
+                          true,                             // Persistent messages
+                          self::$_CELERY_MESSAGE_TIMEOUT,   // Result expiration
+                          array());                         // SSL opts
+    }
+
+    /**
+     * Send an amqp message to Celery the airtime-celery daemon to perform a task
+     *
+     * @param $task     string the Celery task name
+     * @param $exchange string the amqp exchange name
+     * @param $data     array  an associative array containing arguments for the Celery task
+     *
+     * @return string the task identifier for the started Celery task so we can fetch the
+     *                results asynchronously later
+     *
+     * @throws CeleryException when no message is found
+     */
     public static function sendCeleryMessage($task, $exchange, $data) {
-        $CC_CONFIG = Config::getConfig();
+        $config  = Config::getConfig();
+        $queue = $routingKey = $exchange;
+        $c = self::_setupCeleryExchange($config, $exchange, $queue);  // Use the exchange name for the queue
+        $result = $c->PostTask($task, $data, true, $routingKey);      // and routing key
+        return $result->getId();
+    }
 
-        $c = new Celery($CC_CONFIG["rabbitmq"]["host"],
-                        $CC_CONFIG["rabbitmq"]["user"],
-                        $CC_CONFIG["rabbitmq"]["password"],
-                        $CC_CONFIG["rabbitmq"]["vhost"],
-                        $exchange=$exchange);
-        $result = $c->PostTask($task, $data);
+    /**
+     * Given a task name and identifier, check the Celery results queue for any
+     * corresponding messages
+     *
+     * @param $task string the Celery task name
+     * @param $id   string the Celery task identifier
+     *
+     * @return object the message object
+     *
+     * @throws CeleryException when no message is found
+     */
+    public static function getAsyncResultMessage($task, $id) {
+        $config  = Config::getConfig();
+        $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $config["stationId"];
+        $c = self::_setupCeleryExchange($config, self::$_CELERY_RESULTS_EXCHANGE, $queue);
+        $message = $c->getAsyncResultMessage($task, $id);
 
-        $timeout = 0;
-        while(!$result->isReady()) {
-            sleep(1);
-            if($timeout++ >= self::CELERY_TIMEOUT) {
-                break;
-            }
-        }
-
-        if($result->isSuccess()) {
-            Logging::info($result);
-            return $result->getResult();
-        } else {
-            throw new CeleryTimeoutException("Celery task $task timed out!");
+        if ($message == FALSE) {
+            throw new CeleryException("Failed to get message for task $task with ID $id");
         }
+        return $message;
     }
 
     public static function SendMessageToPypo($event_type, $md)
@@ -177,7 +227,4 @@ class Application_Model_RabbitMq
         //XXX: This function has been deprecated and is no longer needed
     }
 
-    public static function uploadToSoundCloud($data) {
-        return self::sendCeleryMessage("upload", "soundcloud-uploads", $data);
-    }
 }
diff --git a/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php b/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
index bf80e6cd1..07f49e86a 100644
--- a/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
@@ -40,8 +40,11 @@ class ThirdPartyTrackReferencesTableMap extends TableMap
         $this->setPrimaryKeyMethodInfo('third_party_track_references_id_seq');
         // columns
         $this->addPrimaryKey('id', 'DbId', 'INTEGER', true, null, null);
-        $this->addColumn('service', 'DbService', 'VARCHAR', true, 512, null);
-        $this->addColumn('foreign_id', 'DbForeignId', 'INTEGER', true, null, null);
+        $this->addColumn('service', 'DbService', 'VARCHAR', true, 256, null);
+        $this->addColumn('foreign_id', 'DbForeignId', 'VARCHAR', false, 256, null);
+        $this->addColumn('broker_task_id', 'DbBrokerTaskId', 'VARCHAR', false, 256, null);
+        $this->addColumn('broker_task_name', 'DbBrokerTaskName', 'VARCHAR', false, 256, null);
+        $this->addColumn('broker_task_dispatch_time', 'DbBrokerTaskDispatchTime', 'TIMESTAMP', false, null, null);
         $this->addForeignKey('file_id', 'DbFileId', 'INTEGER', 'cc_playout_history_template', 'id', true, null, null);
         $this->addColumn('status', 'DbStatus', 'VARCHAR', true, 256, null);
         // validators
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
index b94085171..b880cd0d4 100644
--- a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
@@ -43,10 +43,28 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
 
     /**
      * The value for the foreign_id field.
-     * @var        int
+     * @var        string
      */
     protected $foreign_id;
 
+    /**
+     * The value for the broker_task_id field.
+     * @var        string
+     */
+    protected $broker_task_id;
+
+    /**
+     * The value for the broker_task_name field.
+     * @var        string
+     */
+    protected $broker_task_name;
+
+    /**
+     * The value for the broker_task_dispatch_time field.
+     * @var        string
+     */
+    protected $broker_task_dispatch_time;
+
     /**
      * The value for the file_id field.
      * @var        int
@@ -109,7 +127,7 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     /**
      * Get the [foreign_id] column value.
      *
-     * @return int
+     * @return string
      */
     public function getDbForeignId()
     {
@@ -117,6 +135,63 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         return $this->foreign_id;
     }
 
+    /**
+     * Get the [broker_task_id] column value.
+     *
+     * @return string
+     */
+    public function getDbBrokerTaskId()
+    {
+
+        return $this->broker_task_id;
+    }
+
+    /**
+     * Get the [broker_task_name] column value.
+     *
+     * @return string
+     */
+    public function getDbBrokerTaskName()
+    {
+
+        return $this->broker_task_name;
+    }
+
+    /**
+     * Get the [optionally formatted] temporal [broker_task_dispatch_time] column value.
+     *
+     *
+     * @param string $format The date/time format string (either date()-style or strftime()-style).
+     *				 If format is null, then the raw DateTime object will be returned.
+     * @return mixed Formatted date/time value as string or DateTime object (if format is null), null if column is null
+     * @throws PropelException - if unable to parse/validate the date/time value.
+     */
+    public function getDbBrokerTaskDispatchTime($format = 'Y-m-d H:i:s')
+    {
+        if ($this->broker_task_dispatch_time === null) {
+            return null;
+        }
+
+
+        try {
+            $dt = new DateTime($this->broker_task_dispatch_time);
+        } catch (Exception $x) {
+            throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->broker_task_dispatch_time, true), $x);
+        }
+
+        if ($format === null) {
+            // Because propel.useDateTimeClass is true, we return a DateTime object.
+            return $dt;
+        }
+
+        if (strpos($format, '%') !== false) {
+            return strftime($format, $dt->format('U'));
+        }
+
+        return $dt->format($format);
+
+    }
+
     /**
      * Get the [file_id] column value.
      *
@@ -184,13 +259,13 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     /**
      * Set the value of [foreign_id] column.
      *
-     * @param  int $v new value
+     * @param  string $v new value
      * @return ThirdPartyTrackReferences The current object (for fluent API support)
      */
     public function setDbForeignId($v)
     {
         if ($v !== null && is_numeric($v)) {
-            $v = (int) $v;
+            $v = (string) $v;
         }
 
         if ($this->foreign_id !== $v) {
@@ -202,6 +277,71 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         return $this;
     } // setDbForeignId()
 
+    /**
+     * Set the value of [broker_task_id] column.
+     *
+     * @param  string $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbBrokerTaskId($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->broker_task_id !== $v) {
+            $this->broker_task_id = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::BROKER_TASK_ID;
+        }
+
+
+        return $this;
+    } // setDbBrokerTaskId()
+
+    /**
+     * Set the value of [broker_task_name] column.
+     *
+     * @param  string $v new value
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbBrokerTaskName($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->broker_task_name !== $v) {
+            $this->broker_task_name = $v;
+            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME;
+        }
+
+
+        return $this;
+    } // setDbBrokerTaskName()
+
+    /**
+     * Sets the value of [broker_task_dispatch_time] column to a normalized version of the date/time value specified.
+     *
+     * @param mixed $v string, integer (timestamp), or DateTime value.
+     *               Empty strings are treated as null.
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbBrokerTaskDispatchTime($v)
+    {
+        $dt = PropelDateTime::newInstance($v, null, 'DateTime');
+        if ($this->broker_task_dispatch_time !== null || $dt !== null) {
+            $currentDateAsString = ($this->broker_task_dispatch_time !== null && $tmpDt = new DateTime($this->broker_task_dispatch_time)) ? $tmpDt->format('Y-m-d H:i:s') : null;
+            $newDateAsString = $dt ? $dt->format('Y-m-d H:i:s') : null;
+            if ($currentDateAsString !== $newDateAsString) {
+                $this->broker_task_dispatch_time = $newDateAsString;
+                $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME;
+            }
+        } // if either are not null
+
+
+        return $this;
+    } // setDbBrokerTaskDispatchTime()
+
     /**
      * Set the value of [file_id] column.
      *
@@ -282,9 +422,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
 
             $this->id = ($row[$startcol + 0] !== null) ? (int) $row[$startcol + 0] : null;
             $this->service = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null;
-            $this->foreign_id = ($row[$startcol + 2] !== null) ? (int) $row[$startcol + 2] : null;
-            $this->file_id = ($row[$startcol + 3] !== null) ? (int) $row[$startcol + 3] : null;
-            $this->status = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->foreign_id = ($row[$startcol + 2] !== null) ? (string) $row[$startcol + 2] : null;
+            $this->broker_task_id = ($row[$startcol + 3] !== null) ? (string) $row[$startcol + 3] : null;
+            $this->broker_task_name = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->broker_task_dispatch_time = ($row[$startcol + 5] !== null) ? (string) $row[$startcol + 5] : null;
+            $this->file_id = ($row[$startcol + 6] !== null) ? (int) $row[$startcol + 6] : null;
+            $this->status = ($row[$startcol + 7] !== null) ? (string) $row[$startcol + 7] : null;
             $this->resetModified();
 
             $this->setNew(false);
@@ -294,7 +437,7 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             }
             $this->postHydrate($row, $startcol, $rehydrate);
 
-            return $startcol + 5; // 5 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS.
+            return $startcol + 8; // 8 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS.
 
         } catch (Exception $e) {
             throw new PropelException("Error populating ThirdPartyTrackReferences object", $e);
@@ -541,6 +684,15 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FOREIGN_ID)) {
             $modifiedColumns[':p' . $index++]  = '"foreign_id"';
         }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID)) {
+            $modifiedColumns[':p' . $index++]  = '"broker_task_id"';
+        }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME)) {
+            $modifiedColumns[':p' . $index++]  = '"broker_task_name"';
+        }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME)) {
+            $modifiedColumns[':p' . $index++]  = '"broker_task_dispatch_time"';
+        }
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FILE_ID)) {
             $modifiedColumns[':p' . $index++]  = '"file_id"';
         }
@@ -565,7 +717,16 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                         $stmt->bindValue($identifier, $this->service, PDO::PARAM_STR);
                         break;
                     case '"foreign_id"':
-                        $stmt->bindValue($identifier, $this->foreign_id, PDO::PARAM_INT);
+                        $stmt->bindValue($identifier, $this->foreign_id, PDO::PARAM_STR);
+                        break;
+                    case '"broker_task_id"':
+                        $stmt->bindValue($identifier, $this->broker_task_id, PDO::PARAM_STR);
+                        break;
+                    case '"broker_task_name"':
+                        $stmt->bindValue($identifier, $this->broker_task_name, PDO::PARAM_STR);
+                        break;
+                    case '"broker_task_dispatch_time"':
+                        $stmt->bindValue($identifier, $this->broker_task_dispatch_time, PDO::PARAM_STR);
                         break;
                     case '"file_id"':
                         $stmt->bindValue($identifier, $this->file_id, PDO::PARAM_INT);
@@ -722,9 +883,18 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                 return $this->getDbForeignId();
                 break;
             case 3:
-                return $this->getDbFileId();
+                return $this->getDbBrokerTaskId();
                 break;
             case 4:
+                return $this->getDbBrokerTaskName();
+                break;
+            case 5:
+                return $this->getDbBrokerTaskDispatchTime();
+                break;
+            case 6:
+                return $this->getDbFileId();
+                break;
+            case 7:
                 return $this->getDbStatus();
                 break;
             default:
@@ -759,8 +929,11 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             $keys[0] => $this->getDbId(),
             $keys[1] => $this->getDbService(),
             $keys[2] => $this->getDbForeignId(),
-            $keys[3] => $this->getDbFileId(),
-            $keys[4] => $this->getDbStatus(),
+            $keys[3] => $this->getDbBrokerTaskId(),
+            $keys[4] => $this->getDbBrokerTaskName(),
+            $keys[5] => $this->getDbBrokerTaskDispatchTime(),
+            $keys[6] => $this->getDbFileId(),
+            $keys[7] => $this->getDbStatus(),
         );
         $virtualColumns = $this->virtualColumns;
         foreach ($virtualColumns as $key => $virtualColumn) {
@@ -815,9 +988,18 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                 $this->setDbForeignId($value);
                 break;
             case 3:
-                $this->setDbFileId($value);
+                $this->setDbBrokerTaskId($value);
                 break;
             case 4:
+                $this->setDbBrokerTaskName($value);
+                break;
+            case 5:
+                $this->setDbBrokerTaskDispatchTime($value);
+                break;
+            case 6:
+                $this->setDbFileId($value);
+                break;
+            case 7:
                 $this->setDbStatus($value);
                 break;
         } // switch()
@@ -847,8 +1029,11 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         if (array_key_exists($keys[0], $arr)) $this->setDbId($arr[$keys[0]]);
         if (array_key_exists($keys[1], $arr)) $this->setDbService($arr[$keys[1]]);
         if (array_key_exists($keys[2], $arr)) $this->setDbForeignId($arr[$keys[2]]);
-        if (array_key_exists($keys[3], $arr)) $this->setDbFileId($arr[$keys[3]]);
-        if (array_key_exists($keys[4], $arr)) $this->setDbStatus($arr[$keys[4]]);
+        if (array_key_exists($keys[3], $arr)) $this->setDbBrokerTaskId($arr[$keys[3]]);
+        if (array_key_exists($keys[4], $arr)) $this->setDbBrokerTaskName($arr[$keys[4]]);
+        if (array_key_exists($keys[5], $arr)) $this->setDbBrokerTaskDispatchTime($arr[$keys[5]]);
+        if (array_key_exists($keys[6], $arr)) $this->setDbFileId($arr[$keys[6]]);
+        if (array_key_exists($keys[7], $arr)) $this->setDbStatus($arr[$keys[7]]);
     }
 
     /**
@@ -863,6 +1048,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::ID)) $criteria->add(ThirdPartyTrackReferencesPeer::ID, $this->id);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::SERVICE)) $criteria->add(ThirdPartyTrackReferencesPeer::SERVICE, $this->service);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FOREIGN_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $this->foreign_id);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID, $this->broker_task_id);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME)) $criteria->add(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME, $this->broker_task_name);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME)) $criteria->add(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $this->broker_task_dispatch_time);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FILE_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::FILE_ID, $this->file_id);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::STATUS)) $criteria->add(ThirdPartyTrackReferencesPeer::STATUS, $this->status);
 
@@ -930,6 +1118,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     {
         $copyObj->setDbService($this->getDbService());
         $copyObj->setDbForeignId($this->getDbForeignId());
+        $copyObj->setDbBrokerTaskId($this->getDbBrokerTaskId());
+        $copyObj->setDbBrokerTaskName($this->getDbBrokerTaskName());
+        $copyObj->setDbBrokerTaskDispatchTime($this->getDbBrokerTaskDispatchTime());
         $copyObj->setDbFileId($this->getDbFileId());
         $copyObj->setDbStatus($this->getDbStatus());
 
@@ -1050,6 +1241,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         $this->id = null;
         $this->service = null;
         $this->foreign_id = null;
+        $this->broker_task_id = null;
+        $this->broker_task_name = null;
+        $this->broker_task_dispatch_time = null;
         $this->file_id = null;
         $this->status = null;
         $this->alreadyInSave = false;
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
index 20e769677..079dd9808 100644
--- a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
@@ -24,13 +24,13 @@ abstract class BaseThirdPartyTrackReferencesPeer
     const TM_CLASS = 'ThirdPartyTrackReferencesTableMap';
 
     /** The total number of columns. */
-    const NUM_COLUMNS = 5;
+    const NUM_COLUMNS = 8;
 
     /** The number of lazy-loaded columns. */
     const NUM_LAZY_LOAD_COLUMNS = 0;
 
     /** The number of columns to hydrate (NUM_COLUMNS - NUM_LAZY_LOAD_COLUMNS) */
-    const NUM_HYDRATE_COLUMNS = 5;
+    const NUM_HYDRATE_COLUMNS = 8;
 
     /** the column name for the id field */
     const ID = 'third_party_track_references.id';
@@ -41,6 +41,15 @@ abstract class BaseThirdPartyTrackReferencesPeer
     /** the column name for the foreign_id field */
     const FOREIGN_ID = 'third_party_track_references.foreign_id';
 
+    /** the column name for the broker_task_id field */
+    const BROKER_TASK_ID = 'third_party_track_references.broker_task_id';
+
+    /** the column name for the broker_task_name field */
+    const BROKER_TASK_NAME = 'third_party_track_references.broker_task_name';
+
+    /** the column name for the broker_task_dispatch_time field */
+    const BROKER_TASK_DISPATCH_TIME = 'third_party_track_references.broker_task_dispatch_time';
+
     /** the column name for the file_id field */
     const FILE_ID = 'third_party_track_references.file_id';
 
@@ -66,12 +75,12 @@ abstract class BaseThirdPartyTrackReferencesPeer
      * e.g. ThirdPartyTrackReferencesPeer::$fieldNames[ThirdPartyTrackReferencesPeer::TYPE_PHPNAME][0] = 'Id'
      */
     protected static $fieldNames = array (
-        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbService', 'DbForeignId', 'DbFileId', 'DbStatus', ),
-        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbService', 'dbForeignId', 'dbFileId', 'dbStatus', ),
-        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID, ThirdPartyTrackReferencesPeer::SERVICE, ThirdPartyTrackReferencesPeer::FOREIGN_ID, ThirdPartyTrackReferencesPeer::FILE_ID, ThirdPartyTrackReferencesPeer::STATUS, ),
-        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'SERVICE', 'FOREIGN_ID', 'FILE_ID', 'STATUS', ),
-        BasePeer::TYPE_FIELDNAME => array ('id', 'service', 'foreign_id', 'file_id', 'status', ),
-        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbService', 'DbForeignId', 'DbBrokerTaskId', 'DbBrokerTaskName', 'DbBrokerTaskDispatchTime', 'DbFileId', 'DbStatus', ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbService', 'dbForeignId', 'dbBrokerTaskId', 'dbBrokerTaskName', 'dbBrokerTaskDispatchTime', 'dbFileId', 'dbStatus', ),
+        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID, ThirdPartyTrackReferencesPeer::SERVICE, ThirdPartyTrackReferencesPeer::FOREIGN_ID, ThirdPartyTrackReferencesPeer::BROKER_TASK_ID, ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME, ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, ThirdPartyTrackReferencesPeer::FILE_ID, ThirdPartyTrackReferencesPeer::STATUS, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'SERVICE', 'FOREIGN_ID', 'BROKER_TASK_ID', 'BROKER_TASK_NAME', 'BROKER_TASK_DISPATCH_TIME', 'FILE_ID', 'STATUS', ),
+        BasePeer::TYPE_FIELDNAME => array ('id', 'service', 'foreign_id', 'broker_task_id', 'broker_task_name', 'broker_task_dispatch_time', 'file_id', 'status', ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, )
     );
 
     /**
@@ -81,12 +90,12 @@ abstract class BaseThirdPartyTrackReferencesPeer
      * e.g. ThirdPartyTrackReferencesPeer::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0
      */
     protected static $fieldKeys = array (
-        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbService' => 1, 'DbForeignId' => 2, 'DbFileId' => 3, 'DbStatus' => 4, ),
-        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbService' => 1, 'dbForeignId' => 2, 'dbFileId' => 3, 'dbStatus' => 4, ),
-        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID => 0, ThirdPartyTrackReferencesPeer::SERVICE => 1, ThirdPartyTrackReferencesPeer::FOREIGN_ID => 2, ThirdPartyTrackReferencesPeer::FILE_ID => 3, ThirdPartyTrackReferencesPeer::STATUS => 4, ),
-        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'SERVICE' => 1, 'FOREIGN_ID' => 2, 'FILE_ID' => 3, 'STATUS' => 4, ),
-        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'service' => 1, 'foreign_id' => 2, 'file_id' => 3, 'status' => 4, ),
-        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbService' => 1, 'DbForeignId' => 2, 'DbBrokerTaskId' => 3, 'DbBrokerTaskName' => 4, 'DbBrokerTaskDispatchTime' => 5, 'DbFileId' => 6, 'DbStatus' => 7, ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbService' => 1, 'dbForeignId' => 2, 'dbBrokerTaskId' => 3, 'dbBrokerTaskName' => 4, 'dbBrokerTaskDispatchTime' => 5, 'dbFileId' => 6, 'dbStatus' => 7, ),
+        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID => 0, ThirdPartyTrackReferencesPeer::SERVICE => 1, ThirdPartyTrackReferencesPeer::FOREIGN_ID => 2, ThirdPartyTrackReferencesPeer::BROKER_TASK_ID => 3, ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME => 4, ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME => 5, ThirdPartyTrackReferencesPeer::FILE_ID => 6, ThirdPartyTrackReferencesPeer::STATUS => 7, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'SERVICE' => 1, 'FOREIGN_ID' => 2, 'BROKER_TASK_ID' => 3, 'BROKER_TASK_NAME' => 4, 'BROKER_TASK_DISPATCH_TIME' => 5, 'FILE_ID' => 6, 'STATUS' => 7, ),
+        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'service' => 1, 'foreign_id' => 2, 'broker_task_id' => 3, 'broker_task_name' => 4, 'broker_task_dispatch_time' => 5, 'file_id' => 6, 'status' => 7, ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, )
     );
 
     /**
@@ -163,12 +172,18 @@ abstract class BaseThirdPartyTrackReferencesPeer
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::ID);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::SERVICE);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::FOREIGN_ID);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::FILE_ID);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::STATUS);
         } else {
             $criteria->addSelectColumn($alias . '.id');
             $criteria->addSelectColumn($alias . '.service');
             $criteria->addSelectColumn($alias . '.foreign_id');
+            $criteria->addSelectColumn($alias . '.broker_task_id');
+            $criteria->addSelectColumn($alias . '.broker_task_name');
+            $criteria->addSelectColumn($alias . '.broker_task_dispatch_time');
             $criteria->addSelectColumn($alias . '.file_id');
             $criteria->addSelectColumn($alias . '.status');
         }
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
index 29ac981eb..8602b947b 100644
--- a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
@@ -9,12 +9,18 @@
  * @method ThirdPartyTrackReferencesQuery orderByDbId($order = Criteria::ASC) Order by the id column
  * @method ThirdPartyTrackReferencesQuery orderByDbService($order = Criteria::ASC) Order by the service column
  * @method ThirdPartyTrackReferencesQuery orderByDbForeignId($order = Criteria::ASC) Order by the foreign_id column
+ * @method ThirdPartyTrackReferencesQuery orderByDbBrokerTaskId($order = Criteria::ASC) Order by the broker_task_id column
+ * @method ThirdPartyTrackReferencesQuery orderByDbBrokerTaskName($order = Criteria::ASC) Order by the broker_task_name column
+ * @method ThirdPartyTrackReferencesQuery orderByDbBrokerTaskDispatchTime($order = Criteria::ASC) Order by the broker_task_dispatch_time column
  * @method ThirdPartyTrackReferencesQuery orderByDbFileId($order = Criteria::ASC) Order by the file_id column
  * @method ThirdPartyTrackReferencesQuery orderByDbStatus($order = Criteria::ASC) Order by the status column
  *
  * @method ThirdPartyTrackReferencesQuery groupByDbId() Group by the id column
  * @method ThirdPartyTrackReferencesQuery groupByDbService() Group by the service column
  * @method ThirdPartyTrackReferencesQuery groupByDbForeignId() Group by the foreign_id column
+ * @method ThirdPartyTrackReferencesQuery groupByDbBrokerTaskId() Group by the broker_task_id column
+ * @method ThirdPartyTrackReferencesQuery groupByDbBrokerTaskName() Group by the broker_task_name column
+ * @method ThirdPartyTrackReferencesQuery groupByDbBrokerTaskDispatchTime() Group by the broker_task_dispatch_time column
  * @method ThirdPartyTrackReferencesQuery groupByDbFileId() Group by the file_id column
  * @method ThirdPartyTrackReferencesQuery groupByDbStatus() Group by the status column
  *
@@ -30,13 +36,19 @@
  * @method ThirdPartyTrackReferences findOneOrCreate(PropelPDO $con = null) Return the first ThirdPartyTrackReferences matching the query, or a new ThirdPartyTrackReferences object populated from the query conditions when no match is found
  *
  * @method ThirdPartyTrackReferences findOneByDbService(string $service) Return the first ThirdPartyTrackReferences filtered by the service column
- * @method ThirdPartyTrackReferences findOneByDbForeignId(int $foreign_id) Return the first ThirdPartyTrackReferences filtered by the foreign_id column
+ * @method ThirdPartyTrackReferences findOneByDbForeignId(string $foreign_id) Return the first ThirdPartyTrackReferences filtered by the foreign_id column
+ * @method ThirdPartyTrackReferences findOneByDbBrokerTaskId(string $broker_task_id) Return the first ThirdPartyTrackReferences filtered by the broker_task_id column
+ * @method ThirdPartyTrackReferences findOneByDbBrokerTaskName(string $broker_task_name) Return the first ThirdPartyTrackReferences filtered by the broker_task_name column
+ * @method ThirdPartyTrackReferences findOneByDbBrokerTaskDispatchTime(string $broker_task_dispatch_time) Return the first ThirdPartyTrackReferences filtered by the broker_task_dispatch_time column
  * @method ThirdPartyTrackReferences findOneByDbFileId(int $file_id) Return the first ThirdPartyTrackReferences filtered by the file_id column
  * @method ThirdPartyTrackReferences findOneByDbStatus(string $status) Return the first ThirdPartyTrackReferences filtered by the status column
  *
  * @method array findByDbId(int $id) Return ThirdPartyTrackReferences objects filtered by the id column
  * @method array findByDbService(string $service) Return ThirdPartyTrackReferences objects filtered by the service column
- * @method array findByDbForeignId(int $foreign_id) Return ThirdPartyTrackReferences objects filtered by the foreign_id column
+ * @method array findByDbForeignId(string $foreign_id) Return ThirdPartyTrackReferences objects filtered by the foreign_id column
+ * @method array findByDbBrokerTaskId(string $broker_task_id) Return ThirdPartyTrackReferences objects filtered by the broker_task_id column
+ * @method array findByDbBrokerTaskName(string $broker_task_name) Return ThirdPartyTrackReferences objects filtered by the broker_task_name column
+ * @method array findByDbBrokerTaskDispatchTime(string $broker_task_dispatch_time) Return ThirdPartyTrackReferences objects filtered by the broker_task_dispatch_time column
  * @method array findByDbFileId(int $file_id) Return ThirdPartyTrackReferences objects filtered by the file_id column
  * @method array findByDbStatus(string $status) Return ThirdPartyTrackReferences objects filtered by the status column
  *
@@ -146,7 +158,7 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
      */
     protected function findPkSimple($key, $con)
     {
-        $sql = 'SELECT "id", "service", "foreign_id", "file_id", "status" FROM "third_party_track_references" WHERE "id" = :p0';
+        $sql = 'SELECT "id", "service", "foreign_id", "broker_task_id", "broker_task_name", "broker_task_dispatch_time", "file_id", "status" FROM "third_party_track_references" WHERE "id" = :p0';
         try {
             $stmt = $con->prepare($sql);
             $stmt->bindValue(':p0', $key, PDO::PARAM_INT);
@@ -311,13 +323,101 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
      *
      * Example usage:
      * <code>
-     * $query->filterByDbForeignId(1234); // WHERE foreign_id = 1234
-     * $query->filterByDbForeignId(array(12, 34)); // WHERE foreign_id IN (12, 34)
-     * $query->filterByDbForeignId(array('min' => 12)); // WHERE foreign_id >= 12
-     * $query->filterByDbForeignId(array('max' => 12)); // WHERE foreign_id <= 12
+     * $query->filterByDbForeignId('fooValue');   // WHERE foreign_id = 'fooValue'
+     * $query->filterByDbForeignId('%fooValue%'); // WHERE foreign_id LIKE '%fooValue%'
      * </code>
      *
-     * @param     mixed $dbForeignId The value to use as filter.
+     * @param     string $dbForeignId The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbForeignId($dbForeignId = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbForeignId)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbForeignId)) {
+                $dbForeignId = str_replace('*', '%', $dbForeignId);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId, $comparison);
+    }
+
+    /**
+     * Filter the query on the broker_task_id column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbBrokerTaskId('fooValue');   // WHERE broker_task_id = 'fooValue'
+     * $query->filterByDbBrokerTaskId('%fooValue%'); // WHERE broker_task_id LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbBrokerTaskId The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbBrokerTaskId($dbBrokerTaskId = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbBrokerTaskId)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbBrokerTaskId)) {
+                $dbBrokerTaskId = str_replace('*', '%', $dbBrokerTaskId);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID, $dbBrokerTaskId, $comparison);
+    }
+
+    /**
+     * Filter the query on the broker_task_name column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbBrokerTaskName('fooValue');   // WHERE broker_task_name = 'fooValue'
+     * $query->filterByDbBrokerTaskName('%fooValue%'); // WHERE broker_task_name LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbBrokerTaskName The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbBrokerTaskName($dbBrokerTaskName = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbBrokerTaskName)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbBrokerTaskName)) {
+                $dbBrokerTaskName = str_replace('*', '%', $dbBrokerTaskName);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME, $dbBrokerTaskName, $comparison);
+    }
+
+    /**
+     * Filter the query on the broker_task_dispatch_time column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbBrokerTaskDispatchTime('2011-03-14'); // WHERE broker_task_dispatch_time = '2011-03-14'
+     * $query->filterByDbBrokerTaskDispatchTime('now'); // WHERE broker_task_dispatch_time = '2011-03-14'
+     * $query->filterByDbBrokerTaskDispatchTime(array('max' => 'yesterday')); // WHERE broker_task_dispatch_time < '2011-03-13'
+     * </code>
+     *
+     * @param     mixed $dbBrokerTaskDispatchTime The value to use as filter.
+     *              Values can be integers (unix timestamps), DateTime objects, or strings.
+     *              Empty strings are treated as NULL.
      *              Use scalar values for equality.
      *              Use array values for in_array() equivalent.
      *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
@@ -325,16 +425,16 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
      *
      * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
      */
-    public function filterByDbForeignId($dbForeignId = null, $comparison = null)
+    public function filterByDbBrokerTaskDispatchTime($dbBrokerTaskDispatchTime = null, $comparison = null)
     {
-        if (is_array($dbForeignId)) {
+        if (is_array($dbBrokerTaskDispatchTime)) {
             $useMinMax = false;
-            if (isset($dbForeignId['min'])) {
-                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId['min'], Criteria::GREATER_EQUAL);
+            if (isset($dbBrokerTaskDispatchTime['min'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $dbBrokerTaskDispatchTime['min'], Criteria::GREATER_EQUAL);
                 $useMinMax = true;
             }
-            if (isset($dbForeignId['max'])) {
-                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId['max'], Criteria::LESS_EQUAL);
+            if (isset($dbBrokerTaskDispatchTime['max'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $dbBrokerTaskDispatchTime['max'], Criteria::LESS_EQUAL);
                 $useMinMax = true;
             }
             if ($useMinMax) {
@@ -345,7 +445,7 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
             }
         }
 
-        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId, $comparison);
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $dbBrokerTaskDispatchTime, $comparison);
     }
 
     /**
diff --git a/airtime_mvc/application/services/SoundCloudService.php b/airtime_mvc/application/services/SoundcloudService.php
similarity index 71%
rename from airtime_mvc/application/services/SoundCloudService.php
rename to airtime_mvc/application/services/SoundcloudService.php
index dbfe0c83c..d2f0f0dc8 100644
--- a/airtime_mvc/application/services/SoundCloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -4,6 +4,11 @@ require_once "ThirdPartyService.php";
 
 class SoundcloudService extends ThirdPartyService {
 
+    /**
+     * @var string service access token for accessing remote API
+     */
+    protected $_accessToken;
+
     /**
      * @var Soundcloud\Service SoundCloud API wrapper object
      */
@@ -19,6 +24,16 @@ class SoundcloudService extends ThirdPartyService {
      */
     protected $_THIRD_PARTY_TRACK_URI = 'http://api.soundcloud.com/tracks/';
 
+    /**
+     * @var string exchange name for SoundCloud tasks
+     */
+    protected $_CELERY_EXCHANGE_NAME = 'soundcloud-uploads';
+
+    /**
+     * @var string celery task name for third party uploads
+     */
+    protected $_CELERY_UPLOAD_TASK_NAME = 'upload-to-soundcloud';
+
     /**
      * @var array Application_Model_Preference functions for SoundCloud and their
      *            associated API parameter keys so that we can call them dynamically
@@ -40,35 +55,11 @@ class SoundcloudService extends ThirdPartyService {
         $this->_client = new Soundcloud\Service($clientId, $clientSecret, $redirectUri);
         $accessToken = Application_Model_Preference::getSoundCloudRequestToken();
         if (!empty($accessToken)) {
+            $this->_accessToken = $accessToken;
             $this->_client->setAccessToken($accessToken);
         }
     }
 
-    /**
-     * Upload the file with the given identifier to SoundCloud
-     *
-     * @param int $fileId the local CcFiles identifier
-     *
-     * @throws Soundcloud\Exception\InvalidHttpResponseCodeException
-     *         thrown when the upload fails for any reason
-     */
-    public function upload($fileId) {
-        $file = Application_Model_StoredFile::RecallById($fileId);
-        $data = array(
-            'track_data' => $this->_buildTrackArray($file),
-            'token' => $this->_client->getAccessToken(),
-            'file_path' => $file->getFilePaths()[0]
-        );
-        try {
-            $track = json_decode(Application_Model_RabbitMq::uploadToSoundCloud($data));
-            parent::_createTrackReference($fileId, $track);
-        } catch(Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
-            Logging::info("Invalid request: " . $e->getMessage());
-            // We should only get here if we have an access token, so attempt to refresh
-            $this->accessTokenRefresh();
-        }
-    }
-
     /**
      * Build a parameter array for the track being uploaded to SoundCloud
      *
@@ -76,11 +67,11 @@ class SoundcloudService extends ThirdPartyService {
      *
      * @return array the track array to send to SoundCloud
      */
-    private function _buildTrackArray($file) {
+    protected function _getUploadData($file) {
         $trackArray = array(
             'title' => $file->getName(),
         );
-        foreach($this->_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
+        foreach ($this->_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
             $val = Application_Model_Preference::$func();
             if (!empty($val)) {
                 $trackArray[$param] = $val;
@@ -90,6 +81,34 @@ class SoundcloudService extends ThirdPartyService {
         return $trackArray;
     }
 
+    /**
+     * Update a ThirdPartyTrackReferences object for a completed upload
+     *
+     * @param $fileId int    local CcFiles identifier
+     * @param $track  object third-party service track object
+     *
+     * @throws Exception
+     * @throws PropelException
+     */
+    protected function _addOrUpdateTrackReference($fileId, $track) {
+        // First, check if the track already has an entry in the database
+        $ref = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService($this->_SERVICE_NAME)
+            ->findOneByDbFileId($fileId);
+        if (is_null($ref)) {
+            $ref = new ThirdPartyTrackReferences();
+        }
+        $ref->setDbService($this->_SERVICE_NAME);
+        $ref->setDbForeignId($track->id);
+        $ref->setDbFileId($fileId);
+        $ref->setDbStatus($track->state);
+        // Null the broker task fields because we no longer need them
+        $ref->setDbBrokerTaskId(NULL);
+        $ref->setDbBrokerTaskName(NULL);
+        $ref->setDbBrokerTaskDispatchTime(NULL);
+        $ref->save();
+    }
+
     /**
      * Given a CcFiles identifier for a file that's been uploaded to SoundCloud,
      * return a link to the remote file
@@ -102,7 +121,7 @@ class SoundcloudService extends ThirdPartyService {
         $serviceId = $this->getServiceId($fileId);
         // If we don't find a record for the file we'll get 0 back for the id
         if ($serviceId == 0) { return ''; }
-        $track = json_decode($this->_client->get('tracks/'. $serviceId));
+        $track = json_decode($this->_client->get('tracks/' . $serviceId));
         return $track->permalink_url;
     }
 
@@ -112,8 +131,7 @@ class SoundcloudService extends ThirdPartyService {
      * @return bool true if an access token exists, otherwise false
      */
     public function hasAccessToken() {
-        $accessToken = $this->_client->getAccessToken();
-        return !empty($accessToken);
+        return !empty($this->_accessToken);
     }
 
     /**
@@ -139,6 +157,7 @@ class SoundcloudService extends ThirdPartyService {
         $response = $this->_client->accessToken($code, $postData = array('scope' => 'non-expiring'));
         $accessToken = $response['access_token'];
         Application_Model_Preference::setSoundCloudRequestToken($accessToken);
+        $this->_accessToken = $accessToken;
     }
 
     /**
@@ -150,7 +169,7 @@ class SoundcloudService extends ThirdPartyService {
     public function accessTokenRefresh() {
         assert($this->hasAccessToken());
         try {
-            $accessToken = $this->_client->getAccessToken();
+            $accessToken = $this->_accessToken;
             $this->_client->accessTokenRefresh($accessToken);
         } catch(Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
             // If we get here, then that means our token is stale, so remove it
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
index 48e882a44..dc7497116 100644
--- a/airtime_mvc/application/services/ThirdPartyService.php
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -5,33 +5,79 @@
  */
 abstract class ThirdPartyService {
 
+    /**
+     * @var string service access token for accessing remote API
+     */
+    protected $_accessToken;
+
     /**
      * @var string service name to store in ThirdPartyTrackReferences database
      */
-    protected $_SERVICE_NAME = '';
+    protected $_SERVICE_NAME;
 
     /**
      * @var string base URI for third-party tracks
      */
-    protected $_THIRD_PARTY_TRACK_URI = '';
+    protected $_THIRD_PARTY_TRACK_URI;
+
+    /**
+     * @var string broker exchange name for third party tasks
+     */
+    protected $_CELERY_EXCHANGE_NAME = 'default';
+
+    /**
+     * @var string celery task name for third party uploads
+     */
+    protected $_CELERY_UPLOAD_TASK_NAME = 'upload';
+
+    /**
+     * @var string status string for pending tasks
+     */
+    protected $_PENDING_STATUS = 'PENDING';
+
+    /**
+     * @var string status string for failed tasks
+     */
+    protected $_FAILED_STATUS = 'FAILED';
 
     /**
      * Upload the file with the given identifier to a third-party service
      *
      * @param int $fileId the local CcFiles identifier
+     *
+     * @throws Exception thrown when the upload fails for any reason
      */
-    abstract function upload($fileId);
+    public function upload($fileId) {
+        $file = Application_Model_StoredFile::RecallById($fileId);
+        $data = array(
+            'data' => $this->_getUploadData($file),
+            'token' => $this->_accessToken,
+            'file_path' => $file->getFilePaths()[0]
+        );
+        try {
+            $brokerTaskId = Application_Model_RabbitMq::sendCeleryMessage($this->_CELERY_UPLOAD_TASK_NAME,
+                                                                          $this->_CELERY_EXCHANGE_NAME,
+                                                                          $data);
+            $this->_createTaskReference($fileId, $brokerTaskId, $this->_CELERY_UPLOAD_TASK_NAME);
+        } catch(Exception $e) {
+            Logging::info("Invalid request: " . $e->getMessage());
+            // We should only get here if we have an access token, so attempt to refresh
+            $this->accessTokenRefresh();
+        }
+    }
 
     /**
-     * Create a ThirdPartyTrackReferences and save it to the database
+     * Create a ThirdPartyTrackReferences object for a pending task
      *
-     * @param $fileId int    local CcFiles identifier
-     * @param $track  object third-party service track object
+     * @param $fileId       int    local CcFiles identifier
+     * @param $brokerTaskId int    broker task identifier to so we can asynchronously
+     *                             receive completed task messages
+     * @param $taskName     string broker task name
      *
      * @throws Exception
      * @throws PropelException
      */
-    protected function _createTrackReference($fileId, $track) {
+    protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
         // First, check if the track already has an entry in the database
         $ref = ThirdPartyTrackReferencesQuery::create()
             ->filterByDbService($this->_SERVICE_NAME)
@@ -40,9 +86,12 @@ abstract class ThirdPartyService {
             $ref = new ThirdPartyTrackReferences();
         }
         $ref->setDbService($this->_SERVICE_NAME);
-        $ref->setDbForeignId($track->id);
+        $ref->setDbBrokerTaskId($brokerTaskId);
+        $ref->setDbBrokerTaskName($taskName);
+        $utc = new DateTimeZone("UTC");
+        $ref->setDbBrokerTaskDispatchTime(new DateTime("now", $utc));
         $ref->setDbFileId($fileId);
-        $ref->setDbStatus($track->state);
+        $ref->setDbStatus($this->_PENDING_STATUS);
         $ref->save();
     }
 
@@ -91,6 +140,103 @@ abstract class ThirdPartyService {
         return $serviceId > 0 ? $this->_THIRD_PARTY_TRACK_URI . $serviceId : '';
     }
 
+    /**
+     * Poll the message queue for this service to see if any tasks with the given name have completed
+     * If we find any completed tasks, adjust the ThirdPartyTrackReferences table accordingly
+     * If no task name is passed, we poll all tasks for this service
+     *
+     * @param string $taskName the name of the task to poll for
+     */
+    public function pollBrokerTaskQueue($taskName="") {
+        $pendingTasks = $this->_getPendingTasks($taskName);
+        foreach ($pendingTasks as $task) {
+            try {
+                $result = $this->_getTaskResult($task);
+                Logging::info(json_decode($result));
+                $this->_addOrUpdateTrackReference($task->getDbFileId(), json_decode($result));
+            } catch(CeleryException $e) {
+                Logging::info("Couldn't retrieve task message for task " . $task->getDbBrokerTaskName()
+                              . " with ID " . $task->getDbBrokerTaskId() . ": " . $e->getMessage());
+                if ($this->_checkMessageTimeout($task)) {
+                    $task->setDbStatus($this->_FAILED_STATUS);
+                    $task->save();
+                }
+            }
+        }
+    }
+
+    /**
+     * Return a collection of all pending ThirdPartyTrackReferences to tasks for this service or task
+     *
+     * @param string $taskName the name of the task to look for
+     *
+     * @return PropelCollection any pending ThirdPartyTrackReferences results for this service
+     *                          or task if taskName is provided
+     */
+    protected function _getPendingTasks($taskName) {
+        $query = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService($this->_SERVICE_NAME)
+            ->filterByDbStatus($this->_PENDING_STATUS)
+            ->filterByDbBrokerTaskId('', Criteria::NOT_EQUAL);
+        if (!empty($taskName)) {
+            $query->filterByDbBrokerTaskName($taskName);
+        }
+        return $query->find();
+    }
+
+    /**
+     * Get the result from a celery task message in the results queue
+     * If the task message no longer exists, remove it from the track references table
+     *
+     * @param $task ThirdPartyTrackReferences the track reference object
+     *
+     * @return array the results from the task message
+     *
+     * @throws CeleryException when the result message for this task no longer exists
+     */
+    protected function _getTaskResult($task) {
+        $message = Application_Model_RabbitMq::getAsyncResultMessage($task->getDbBrokerTaskName(),
+                                                                     $task->getDbBrokerTaskId());
+        return json_decode($message['body'])->result;  // The actual result message from the service
+    }
+
+    /**
+     * Check if a task message has been unreachable for more our timeout time
+     *
+     * @param $task ThirdPartyTrackReferences the track reference object
+     *
+     * @return bool true if the dispatch time is empty or it's been more than our timeout time
+     *              since the message was dispatched, otherwise false
+     */
+    protected function _checkMessageTimeout($task) {
+        $utc = new DateTimeZone("UTC");
+        $dispatchTime = new DateTime($task->getDbBrokerTaskDispatchTime(), $utc);
+        $now = new DateTime("now", $utc);
+        $timeoutSeconds = Application_Model_RabbitMq::$_CELERY_MESSAGE_TIMEOUT / 1000;  // Convert from milliseconds
+        $timeoutInterval = new DateInterval("PT" . $timeoutSeconds . "S");
+        return (empty($dispatchTime) || $dispatchTime->add($timeoutInterval) <= $now);
+    }
+
+    /**
+     * Build a parameter array for the file being uploaded to a third party service
+     *
+     * @param $file Application_Model_StoredFile the file being uploaded
+     *
+     * @return array the track array to send to the third party service
+     */
+    abstract protected function _getUploadData($file);
+
+    /**
+     * Update a ThirdPartyTrackReferences object for a completed task
+     *
+     * @param $fileId int    local CcFiles identifier
+     * @param $track  object third-party service track object
+     *
+     * @throws Exception
+     * @throws PropelException
+     */
+    abstract protected function _addOrUpdateTrackReference($fileId, $track);
+
     /**
      * Check whether an OAuth access token exists for the third-party client
      *
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index 03bad21cb..53a479dc9 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -533,10 +533,20 @@
   </table>
   <table name="third_party_track_references" phpName="ThirdPartyTrackReferences">
     <column name="id" phpName="DbId" primaryKey="true" type="INTEGER" autoIncrement="true" required="true" />
-    <column name="service" phpName="DbService" type="VARCHAR" size="512" required="true" />
-    <column name="foreign_id" phpName="DbForeignId" type="INTEGER" required="true" />
+    <column name="service" phpName="DbService" type="VARCHAR" size="256" required="true" />
+    <!-- Make foreign ID a VARCHAR field in case a service uses hashes or other non-integer identifiers -->
+    <column name="foreign_id" phpName="DbForeignId" type="VARCHAR" size="256" />
+    <column name="broker_task_id" phpName="DbBrokerTaskId" type="VARCHAR" size="256" />
+    <column name="broker_task_name" phpName="DbBrokerTaskName" type="VARCHAR" size="256" />
+    <column name="broker_task_dispatch_time" phpName="DbBrokerTaskDispatchTime" type="TIMESTAMP" />
     <column name="file_id" phpName="DbFileId" type="INTEGER" required="true" />
     <column name="status" phpName="DbStatus" type="VARCHAR" size="256" required="true" />
+    <unique name="broker_task_id_unique">
+      <unique-column name="broker_task_id"/>
+    </unique>
+    <unique name="foreign_id_unique">
+      <unique-column name="foreign_id"/>
+    </unique>
     <foreign-key foreignTable="cc_playout_history_template" name="track_reference_fkey" onDelete="CASCADE">
       <reference local="file_id" foreign="id"/>
     </foreign-key>
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index 3d7aaf39a..74a39e597 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -679,11 +679,16 @@ DROP TABLE IF EXISTS "third_party_track_references" CASCADE;
 CREATE TABLE "third_party_track_references"
 (
     "id" serial NOT NULL,
-    "service" VARCHAR(512) NOT NULL,
-    "foreign_id" INTEGER NOT NULL,
+    "service" VARCHAR(256) NOT NULL,
+    "foreign_id" VARCHAR(256),
+    "broker_task_id" VARCHAR(256),
+    "broker_task_name" VARCHAR(256),
+    "broker_task_dispatch_time" TIMESTAMP,
     "file_id" INTEGER NOT NULL,
     "status" VARCHAR(256) NOT NULL,
-    PRIMARY KEY ("id")
+    PRIMARY KEY ("id"),
+    CONSTRAINT "broker_task_id_unique" UNIQUE ("broker_task_id"),
+    CONSTRAINT "foreign_id_unique" UNIQUE ("foreign_id")
 );
 
 ALTER TABLE "cc_files" ADD CONSTRAINT "cc_files_owner_fkey"
diff --git a/airtime_mvc/public/js/airtime/common/common.js b/airtime_mvc/public/js/airtime/common/common.js
index 31f4668bf..2cb0501fe 100644
--- a/airtime_mvc/public/js/airtime/common/common.js
+++ b/airtime_mvc/public/js/airtime/common/common.js
@@ -9,6 +9,7 @@ $(document).ready(function() {
 
     //this statement tells the browser to fade out any success message after 5 seconds
     setTimeout(function(){$(".success").fadeOut("slow", function(){$(this).empty()});}, 5000);
+    pollTaskQueues();
 });
 
 /*
@@ -156,3 +157,8 @@ function removeSuccessMsg() {
     
     $status.fadeOut("slow", function(){$status.empty()});
 }
+
+function pollTaskQueues() {
+    console.log("Polling broker queues...");
+    $.get(baseUrl + 'soundcloud/poll-broker-task-queue');
+}
\ No newline at end of file
diff --git a/python_apps/airtime-celery/airtime-celery/__init__.py b/python_apps/airtime-celery/airtime-celery/__init__.py
new file mode 100644
index 000000000..a65fa3c85
--- /dev/null
+++ b/python_apps/airtime-celery/airtime-celery/__init__.py
@@ -0,0 +1,3 @@
+import os
+# Make the celeryconfig module visible to celery
+os.environ['CELERY_CONFIG_MODULE'] = 'airtime-celery.celeryconfig'
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py b/python_apps/airtime-celery/airtime-celery/celeryconfig.py
similarity index 69%
rename from python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py
rename to python_apps/airtime-celery/airtime-celery/celeryconfig.py
index 08e67821f..fcdd83a70 100644
--- a/python_apps/soundcloud_uploader/soundcloud_uploader/celeryconfig.py
+++ b/python_apps/airtime-celery/airtime-celery/celeryconfig.py
@@ -24,15 +24,22 @@ def get_rmq_broker():
 # Celery amqp settings
 BROKER_URL = get_rmq_broker()
 CELERY_RESULT_BACKEND = 'amqp'     # Use RabbitMQ as the celery backend
-# CELERY_RESULT_EXCHANGE = 'upload-results'
 CELERY_RESULT_PERSISTENT = True    # Persist through a broker restart
-CELERY_TASK_RESULT_EXPIRES = None  # Don't expire tasks
+CELERY_TASK_RESULT_EXPIRES = 300   # Expire task results after 5 minutes
 CELERY_TRACK_STARTED = False
+CELERY_RESULT_EXCHANGE = 'airtime-results'
 CELERY_QUEUES = (
-    Queue('soundcloud-uploads', Exchange('soundcloud-uploads'), routing_key='celery'),
+    Queue('soundcloud-uploads', exchange=Exchange('soundcloud-uploads'), routing_key='soundcloud-uploads'),
+    Queue('airtime-results.soundcloud-uploads', exchange=Exchange('airtime-results')),
+)
+CELERY_ROUTES = (
+    {
+        'soundcloud_uploads.uploader.upload_to_soundcloud': {
+            'exchange': 'airtime-results',
+            'queue': 'airtime-results.soundcloud-uploads',
+        }
+    },
 )
-CELERY_DEFAULT_QUEUE = 'soundcloud-uploads'
-CELERY_DEFAULT_EXCHANGE_TYPE = 'topic'
 
 # Celery task settings
 CELERY_TASK_SERIALIZER = 'json'
diff --git a/python_apps/airtime-celery/airtime-celery/uploader.py b/python_apps/airtime-celery/airtime-celery/uploader.py
new file mode 100644
index 000000000..addbc964d
--- /dev/null
+++ b/python_apps/airtime-celery/airtime-celery/uploader.py
@@ -0,0 +1,24 @@
+import os
+import json
+import urllib2
+import soundcloud
+from celery import Celery
+from celery.utils.log import get_task_logger
+
+celery = Celery()
+logger = get_task_logger(__name__)
+
+
+@celery.task(name='upload-to-soundcloud')
+def upload_to_soundcloud(data, token, file_path):
+    client = soundcloud.Client(access_token=token)
+    # Open the file with urllib2 if it's a cloud file
+    data['asset_data'] = open(file_path, 'rb') if os.path.isfile(file_path) else urllib2.urlopen(file_path)
+    try:
+        logger.info('Uploading track: {0}'.format(data))
+        track = client.post('/tracks', track=data)
+    except Exception as e:
+        logger.info('Error uploading track {title}: {0}'.format(e.message, **data))
+        raise e
+    data['asset_data'].close()
+    return json.dumps(track.fields())
diff --git a/python_apps/soundcloud_uploader/install/conf/soundcloud_uploader b/python_apps/airtime-celery/install/conf/airtime-celery
similarity index 89%
rename from python_apps/soundcloud_uploader/install/conf/soundcloud_uploader
rename to python_apps/airtime-celery/install/conf/airtime-celery
index 4276e402f..b026a8069 100644
--- a/python_apps/soundcloud_uploader/install/conf/soundcloud_uploader
+++ b/python_apps/airtime-celery/install/conf/airtime-celery
@@ -1,11 +1,11 @@
 # Names of nodes to start
-CELERYD_NODES="soundcloud_uploader"
+CELERYD_NODES="airtime-celery"
 
 # Absolute or relative path to the 'celery' command:
 CELERY_BIN="/usr/local/bin/celery"
 
 # App instance to use
-CELERY_APP="soundcloud_uploader.uploader:celery"
+CELERY_APP="airtime-celery.uploader:celery"
 
 # Extra command-line arguments to the worker
 CELERYD_OPTS="--time-limit=300 --concurrency=8 --config=celeryconfig"
diff --git a/python_apps/soundcloud_uploader/install/upstart/soundcloud_uploader b/python_apps/airtime-celery/install/upstart/airtime-celery
similarity index 100%
rename from python_apps/soundcloud_uploader/install/upstart/soundcloud_uploader
rename to python_apps/airtime-celery/install/upstart/airtime-celery
diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
new file mode 100644
index 000000000..6f40351b7
--- /dev/null
+++ b/python_apps/airtime-celery/setup.py
@@ -0,0 +1,45 @@
+from setuptools import setup
+from subprocess import call
+import os
+import sys
+
+install_args = ['install', 'install_data', 'develop']
+
+# Definitely not the best way of doing this...
+if sys.argv[1] in install_args:
+    data_files = [('/etc/default', ['install/conf/airtime-celery']),
+                  ('/etc/init.d', ['install/upstart/airtime-celery'])]
+else:
+    data_files = []
+
+
+def postinst():
+    print "Reloading initctl configuration"
+    call(['initctl', 'reload-configuration'])
+    # Make /etc/init.d file executable and set proper
+    # permissions for the defaults config file
+    os.chmod('/etc/init.d/airtime-celery', 0755)
+    os.chmod('/etc/default/airtime-celery', 0640)
+    print "Setting uploader to start on boot"
+    call(['update-rc.d', 'airtime-celery', 'defaults'])
+    print "Run \"sudo service airtime-celery restart\" now."
+
+setup(name='airtime-celery',
+      version='0.1',
+      description='Airtime Celery service',
+      url='http://github.com/sourcefabric/Airtime',
+      author='Sourcefabric',
+      author_email='duncan.sommerville@sourcefabric.org',
+      license='MIT',
+      packages=['airtime-celery'],
+      install_requires=[
+          'soundcloud',
+          'celery',
+          'kombu'
+      ],
+      zip_safe=False,
+      data_files=data_files)
+
+if data_files:
+    postinst()
+
diff --git a/python_apps/soundcloud_uploader/bin/soundcloud_uploader b/python_apps/soundcloud_uploader/bin/soundcloud_uploader
deleted file mode 100644
index 7ecdd2b88..000000000
--- a/python_apps/soundcloud_uploader/bin/soundcloud_uploader
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env sh
-
-if [ "$(id -u)" != "0" ]; then
-    echo "Please run as root user."
-    exit 1
-fi
-
-service soundcloud_uploader restart
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/setup.py b/python_apps/soundcloud_uploader/setup.py
deleted file mode 100644
index 5625f7c89..000000000
--- a/python_apps/soundcloud_uploader/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from setuptools import setup
-from subprocess import call
-import os
-
-data_files = [('/etc/default', ['install/conf/soundcloud_uploader']),
-              ('/etc/init.d', ['install/upstart/soundcloud_uploader'])]
-print data_files
-
-setup(name='soundcloud_uploader',
-      version='0.1',
-      description='Celery SoundCloud upload worker',
-      url='http://github.com/sourcefabric/Airtime',
-      author='Sourcefabric',
-      author_email='duncan.sommerville@sourcefabric.org',
-      license='MIT',
-      packages=['soundcloud_uploader'],
-      scripts=['bin/soundcloud_uploader'],
-      install_requires=[
-          'soundcloud',
-          'celery',
-          'kombu'
-      ],
-      zip_safe=False,
-      data_files=data_files)
-
-if data_files:
-    print "Reloading initctl configuration"
-    call(['initctl', 'reload-configuration'])
-    # Make /etc/init.d file executable and set proper
-    # permissions for the defaults config file
-    os.chmod('/etc/init.d/soundcloud_uploader', 0755)
-    os.chmod('/etc/default/soundcloud_uploader', 0640)
-    print "Setting uploader to start on boot"
-    call(['update-rc.d', 'soundcloud_uploader', 'defaults'])
-    print "Run \"sudo service soundcloud_uploader restart\" now."
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py b/python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py
deleted file mode 100644
index d7484a82e..000000000
--- a/python_apps/soundcloud_uploader/soundcloud_uploader/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-import os
-# Make the celeryconfig module visible to celery
-os.environ['CELERY_CONFIG_MODULE'] = 'soundcloud_uploader.celeryconfig'
\ No newline at end of file
diff --git a/python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py b/python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py
deleted file mode 100644
index 4fc24c21b..000000000
--- a/python_apps/soundcloud_uploader/soundcloud_uploader/uploader.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import os
-import json
-import urllib2
-import soundcloud
-from celery import Celery
-from celery.utils.log import get_task_logger
-
-celery = Celery('uploader')
-logger = get_task_logger(__name__)
-
-
-@celery.task(queue='soundcloud-uploads', name='upload')
-def upload(track_data, token, file_path):
-    client = soundcloud.Client(access_token=token)
-    # Open the file with urllib2 if it's a cloud file
-    track_data['asset_data'] = open(file_path, 'rb') if os.path.isfile(file_path) else urllib2.urlopen(file_path)
-    try:
-        logger.info('Uploading track: {0}'.format(track_data))
-        track = client.post('/tracks', track=track_data)
-    except Exception as e:
-        logger.info('Error uploading track {name}: {0}'.format(e.message, **track_data))
-        raise e
-    track_data['asset_data'].close()
-    return json.dumps(track.fields())

From c1b5b53a16e8e4c2a53448bf2eede5e4e1e0c627 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 10 Jun 2015 17:11:42 -0400
Subject: [PATCH 06/39] Fix merge errors; use Celery status messages + handle
 fail case better

---
 .../services/SoundcloudService.php            | 16 ++++++++---
 .../services/ThirdPartyService.php            | 27 +++++++++++--------
 airtime_mvc/application/upgrade/Upgrades.php  | 10 +++----
 3 files changed, 33 insertions(+), 20 deletions(-)

diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index d2f0f0dc8..27003cac7 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -83,15 +83,16 @@ class SoundcloudService extends ThirdPartyService {
 
     /**
      * Update a ThirdPartyTrackReferences object for a completed upload
+     * TODO: should we have a database layer class to handle Propel operations?
      *
      * @param $fileId int    local CcFiles identifier
      * @param $track  object third-party service track object
+     * @param $status string Celery task status
      *
      * @throws Exception
      * @throws PropelException
      */
-    protected function _addOrUpdateTrackReference($fileId, $track) {
-        // First, check if the track already has an entry in the database
+    protected function _addOrUpdateTrackReference($fileId, $track, $status) {
         $ref = ThirdPartyTrackReferencesQuery::create()
             ->filterByDbService($this->_SERVICE_NAME)
             ->findOneByDbFileId($fileId);
@@ -99,10 +100,17 @@ class SoundcloudService extends ThirdPartyService {
             $ref = new ThirdPartyTrackReferences();
         }
         $ref->setDbService($this->_SERVICE_NAME);
-        $ref->setDbForeignId($track->id);
+        // Only set the SoundCloud fields if the task was successful
+        if ($status == $this->_SUCCESS_STATUS) {
+            // TODO: fetch any additional SoundCloud parameters we want to store
+            $ref->setDbForeignId($track->id);  // SoundCloud identifier
+        }
         $ref->setDbFileId($fileId);
-        $ref->setDbStatus($track->state);
+        $ref->setDbStatus($status);
         // Null the broker task fields because we no longer need them
+        // We use NULL over an empty string/object here because we have
+        // a unique constraint on the task ID and it's easier to filter
+        // and query against NULLs
         $ref->setDbBrokerTaskId(NULL);
         $ref->setDbBrokerTaskName(NULL);
         $ref->setDbBrokerTaskDispatchTime(NULL);
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
index dc7497116..7ab668db4 100644
--- a/airtime_mvc/application/services/ThirdPartyService.php
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -35,6 +35,11 @@ abstract class ThirdPartyService {
      */
     protected $_PENDING_STATUS = 'PENDING';
 
+    /**
+     * @var string status string for successful tasks
+     */
+    protected $_SUCCESS_STATUS = 'SUCCESS';
+
     /**
      * @var string status string for failed tasks
      */
@@ -68,6 +73,7 @@ abstract class ThirdPartyService {
 
     /**
      * Create a ThirdPartyTrackReferences object for a pending task
+     * TODO: should we have a database layer class to handle Propel operations?
      *
      * @param $fileId       int    local CcFiles identifier
      * @param $brokerTaskId int    broker task identifier to so we can asynchronously
@@ -151,9 +157,8 @@ abstract class ThirdPartyService {
         $pendingTasks = $this->_getPendingTasks($taskName);
         foreach ($pendingTasks as $task) {
             try {
-                $result = $this->_getTaskResult($task);
-                Logging::info(json_decode($result));
-                $this->_addOrUpdateTrackReference($task->getDbFileId(), json_decode($result));
+                $message = $this->_getTaskMessage($task);
+                $this->_addOrUpdateTrackReference($task->getDbFileId(), json_decode($message->result), $message->status);
             } catch(CeleryException $e) {
                 Logging::info("Couldn't retrieve task message for task " . $task->getDbBrokerTaskName()
                               . " with ID " . $task->getDbBrokerTaskId() . ": " . $e->getMessage());
@@ -185,19 +190,18 @@ abstract class ThirdPartyService {
     }
 
     /**
-     * Get the result from a celery task message in the results queue
-     * If the task message no longer exists, remove it from the track references table
+     * Get a Celery task message from the results queue
      *
      * @param $task ThirdPartyTrackReferences the track reference object
      *
-     * @return array the results from the task message
+     * @return object the task message object
      *
      * @throws CeleryException when the result message for this task no longer exists
      */
-    protected function _getTaskResult($task) {
-        $message = Application_Model_RabbitMq::getAsyncResultMessage($task->getDbBrokerTaskName(),
-                                                                     $task->getDbBrokerTaskId());
-        return json_decode($message['body'])->result;  // The actual result message from the service
+    protected function _getTaskMessage($task) {
+        $message =  Application_Model_RabbitMq::getAsyncResultMessage($task->getDbBrokerTaskName(),
+                                                                      $task->getDbBrokerTaskId());
+        return json_decode($message['body']);
     }
 
     /**
@@ -231,11 +235,12 @@ abstract class ThirdPartyService {
      *
      * @param $fileId int    local CcFiles identifier
      * @param $track  object third-party service track object
+     * @param $status string Celery task status
      *
      * @throws Exception
      * @throws PropelException
      */
-    abstract protected function _addOrUpdateTrackReference($fileId, $track);
+    abstract protected function _addOrUpdateTrackReference($fileId, $track, $status);
 
     /**
      * Check whether an OAuth access token exists for the third-party client
diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 774b9cff8..d5eb4e7c2 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -35,7 +35,7 @@ class UpgradeManager
     {
         $schemaVersion = Application_Model_Preference::GetSchemaVersion();
         $supportedSchemaVersions = self::getSupportedSchemaVersions();
-        $upgradeNeeded = !in_array($schemaVersion, $supportedSchemaVersions);
+        return !in_array($schemaVersion, $supportedSchemaVersions);
         // We shouldn't run the upgrade as a side-effect of this function!
         /*
         if ($upgradeNeeded) {
@@ -46,7 +46,6 @@ class UpgradeManager
 
     public function doUpgrade()
     {
-        $didWePerformAnUpgrade = false;
         // Get all upgrades dynamically (in declaration order!) so we don't have to add them explicitly each time
         // TODO: explicitly sort classnames by ascending version suffix for safety
         $upgraders = getUpgrades();
@@ -69,7 +68,7 @@ class UpgradeManager
             {
                 // pass the given directory to the upgrades, since __DIR__ returns parent dir of file, not executor
                 $upgrader->upgrade($dir); //This will throw an exception if the upgrade fails.
-                $didWePerformAnUpgrade = true;
+                $upgradePerformed = true;
             }
         }
         return $upgradePerformed;
@@ -512,7 +511,7 @@ class AirtimeUpgrader2512 extends AirtimeUpgrader
 
 class AirtimeUpgrader2513 extends AirtimeUpgrader
 {
-    protected function getSupportedVersions() {
+    protected function getSupportedSchemaVersions() {
         return array (
             '2.5.12'
         );
@@ -544,7 +543,7 @@ class AirtimeUpgrader2513 extends AirtimeUpgrader
             passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
                      .$newVersion."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
 
-            Application_Model_Preference::SetAirtimeVersion($newVersion);
+            Application_Model_Preference::SetSchemaVersion($newVersion);
             Cache::clear();
 
             $this->toggleMaintenanceScreen(false);
@@ -553,5 +552,6 @@ class AirtimeUpgrader2513 extends AirtimeUpgrader
             throw $e;
         }
     }
+
 }
 

From 15c7ef5885e372a421117cbf8addf25d8c682345 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Fri, 12 Jun 2015 12:31:55 -0400
Subject: [PATCH 07/39] Celery backend and support for dev-env worker
 parallelization

---
 airtime_mvc/application/models/RabbitMq.php   |  21 +-
 airtime_mvc/application/upgrade/Upgrades.php  | 454 +++++-------------
 airtime_mvc/tests/airtime.conf                |   1 +
 python_apps/airtime-celery/README.rst         |  49 ++
 .../airtime-celery/celeryconfig.py            |  26 +-
 .../airtime-celery/{uploader.py => tasks.py}  |  10 +
 .../install/conf/airtime-celery               |   4 +-
 .../install/conf/airtime-celery-bananas       |  25 +
 .../install/conf/airtime-celery-cliff         |  25 +
 .../install/conf/airtime-celery-production    |  25 +
 .../install/conf/airtime-celery-staging       |  25 +
 .../install/{upstart => initd}/airtime-celery |   1 +
 .../install/initd/airtime-celery-bananas      | 334 +++++++++++++
 .../install/initd/airtime-celery-cliff        | 334 +++++++++++++
 .../install/initd/airtime-celery-production   | 334 +++++++++++++
 .../install/initd/airtime-celery-staging      | 334 +++++++++++++
 python_apps/airtime-celery/setup.py           |  30 +-
 17 files changed, 1664 insertions(+), 368 deletions(-)
 create mode 100644 python_apps/airtime-celery/README.rst
 rename python_apps/airtime-celery/airtime-celery/{uploader.py => tasks.py} (73%)
 create mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-bananas
 create mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-cliff
 create mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-production
 create mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-staging
 rename python_apps/airtime-celery/install/{upstart => initd}/airtime-celery (99%)
 create mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-bananas
 create mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-cliff
 create mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-production
 create mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-staging

diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php
index da949cd88..0c618861e 100644
--- a/airtime_mvc/application/models/RabbitMq.php
+++ b/airtime_mvc/application/models/RabbitMq.php
@@ -74,8 +74,7 @@ class Application_Model_RabbitMq
                           $config["rabbitmq"]["port"],
                           false,                            // Connector
                           true,                             // Persistent messages
-                          self::$_CELERY_MESSAGE_TIMEOUT,   // Result expiration
-                          array());                         // SSL opts
+                          self::$_CELERY_MESSAGE_TIMEOUT);  // Result expiration
     }
 
     /**
@@ -91,7 +90,7 @@ class Application_Model_RabbitMq
      * @throws CeleryException when no message is found
      */
     public static function sendCeleryMessage($task, $exchange, $data) {
-        $config  = Config::getConfig();
+        $config = parse_ini_file($this->_getRmqConfigPath(), true);
         $queue = $routingKey = $exchange;
         $c = self::_setupCeleryExchange($config, $exchange, $queue);  // Use the exchange name for the queue
         $result = $c->PostTask($task, $data, true, $routingKey);      // and routing key
@@ -110,7 +109,7 @@ class Application_Model_RabbitMq
      * @throws CeleryException when no message is found
      */
     public static function getAsyncResultMessage($task, $id) {
-        $config  = Config::getConfig();
+        $config = parse_ini_file($this->_getRmqConfigPath(), true);
         $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $config["stationId"];
         $c = self::_setupCeleryExchange($config, self::$_CELERY_RESULTS_EXCHANGE, $queue);
         $message = $c->getAsyncResultMessage($task, $id);
@@ -158,12 +157,10 @@ class Application_Model_RabbitMq
         self::sendMessage($exchange, 'direct', true, $data);
     }
 
-    public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename,
-                                                $callbackUrl, $apiKey, $storageBackend, $filePrefix)
-    {
+    private function _getRmqConfigPath() {
         //Hack for Airtime Pro. The RabbitMQ settings for communicating with airtime_analyzer are global
         //and shared between all instances on Airtime Pro.
-        $CC_CONFIG = Config::getConfig();        
+        $CC_CONFIG = Config::getConfig();
         $devEnv = "production"; //Default
         if (array_key_exists("dev_env", $CC_CONFIG)) {
             $devEnv = $CC_CONFIG["dev_env"];
@@ -174,7 +171,13 @@ class Application_Model_RabbitMq
             // to the production rabbitmq-analyzer.ini
             $rmq_config_path = "/etc/airtime-saas/production/rabbitmq-analyzer.ini";
         }
-        $config = parse_ini_file($rmq_config_path, true);
+        return $rmq_config_path;
+    }
+
+    public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename,
+                                                $callbackUrl, $apiKey, $storageBackend, $filePrefix)
+    {
+        $config = parse_ini_file($this->_getRmqConfigPath(), true);
         $conn = new AMQPConnection($config["rabbitmq"]["host"],
                 $config["rabbitmq"]["port"],
                 $config["rabbitmq"]["user"],
diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index d5eb4e7c2..21cad0e71 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -44,42 +44,53 @@ class UpgradeManager
         */
     }
 
+    /**
+     * Upgrade the Airtime schema version to match the highest supported version
+     *
+     * @return boolean whether or not an upgrade was performed
+     */
     public function doUpgrade()
     {
         // Get all upgrades dynamically (in declaration order!) so we don't have to add them explicitly each time
         // TODO: explicitly sort classnames by ascending version suffix for safety
         $upgraders = getUpgrades();
-        return $this->runUpgrades($upgraders, (dirname(__DIR__) . "/controllers"));
+        $dir = (dirname(__DIR__) . "/controllers");
+        $upgradePerformed = false;
+
+        foreach ($upgraders as $upgrader) {
+            $upgradePerformed = $this->_runUpgrade(new $upgrader($dir)) ? true : $upgradePerformed;
+        }
+
+        return $upgradePerformed;
     }
 
     /**
-     * Run a given set of upgrades
-     * 
-     * @param array $upgraders the upgrades to perform
-     * @param string $dir the directory containing the upgrade sql
-     * @return boolean whether or not an upgrade was performed
+     * Run the given upgrade
+     *
+     * @param $upgrader AirtimeUpgrader the upgrade class to be executed
+     *
+     * @return bool true if the upgrade was successful, otherwise false
      */
-    public function runUpgrades($upgraders, $dir) {
-        $upgradePerformed = false;
-        foreach ($upgraders as $upgrader) {
-            /** @var $upgrader AirtimeUpgrader */
-            $upgrader = new $upgrader();
-            if ($upgrader->checkIfUpgradeSupported())
-            {
-                // pass the given directory to the upgrades, since __DIR__ returns parent dir of file, not executor
-                $upgrader->upgrade($dir); //This will throw an exception if the upgrade fails.
-                $upgradePerformed = true;
-            }
-        }
-        return $upgradePerformed;
+    private function _runUpgrade(AirtimeUpgrader $upgrader) {
+        return $upgrader->checkIfUpgradeSupported() && $upgrader->upgrade();
     }
 
 }
 
 abstract class AirtimeUpgrader
 {
+    protected $_dir;
+
+    /**
+     * @param $dir string directory housing upgrade files
+     */
+    public function __construct($dir) {
+        $this->_dir = $dir;
+    }
+
     /** Schema versions that this upgrader class can upgrade from (an array of version strings). */
     abstract protected function getSupportedSchemaVersions();
+
     /** The schema version that this upgrader class will upgrade to. (returns a version string) */
     abstract public function getNewVersion();
 
@@ -94,10 +105,7 @@ abstract class AirtimeUpgrader
      */
     public function checkIfUpgradeSupported()
     {
-        if (!in_array(AirtimeUpgrader::getCurrentSchemaVersion(), $this->getSupportedSchemaVersions())) {
-            return false;
-        }
-        return true;
+        return in_array(AirtimeUpgrader::getCurrentSchemaVersion(), $this->getSupportedSchemaVersions());
     }
 
     protected function toggleMaintenanceScreen($toggle)
@@ -122,8 +130,45 @@ abstract class AirtimeUpgrader
         }
     }
 
-    /** Implement this for each new version of Airtime */
-    abstract public function upgrade();
+    /**
+     * Implement this for each new version of Airtime
+     * This function abstracts out the core upgrade functionality,
+     * allowing child classes to overwrite _runUpgrade to reduce duplication
+     */
+    public function upgrade() {
+        Cache::clear();
+        assert($this->checkIfUpgradeSupported());
+
+        try {
+            // $this->toggleMaintenanceScreen(true);
+            Cache::clear();
+
+            $this->_runUpgrade();
+
+            Application_Model_Preference::SetSchemaVersion($this->getNewVersion());
+            Cache::clear();
+
+            // $this->toggleMaintenanceScreen(false);
+        } catch(Exception $e) {
+            // $this->toggleMaintenanceScreen(false);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function _runUpgrade() {
+        $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
+        $values = parse_ini_file($airtimeConf, true);
+
+        $username = $values['database']['dbuser'];
+        $password = $values['database']['dbpass'];
+        $host = $values['database']['host'];
+        $database = $values['database']['dbname'];
+
+        passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f ".$this->_dir."/upgrade_sql/airtime_"
+                 .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
+    }
 }
 
 class AirtimeUpgrader253 extends AirtimeUpgrader
@@ -137,60 +182,17 @@ class AirtimeUpgrader253 extends AirtimeUpgrader
     {
         return '2.5.3';
     }
-    
-    public function upgrade($dir = __DIR__)
-    {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-        
-        $con = Propel::getConnection();
-        $con->beginTransaction();
-        try {
-            
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-            
-            //Begin upgrade
-        
-            //Update disk_usage value in cc_pref
-            $musicDir = CcMusicDirsQuery::create()
-            ->filterByType('stor')
-            ->filterByExists(true)
-            ->findOne();
-            $storPath = $musicDir->getDirectory();
-        
-            //Update disk_usage value in cc_pref
-            $storDir = isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."srv/airtime/stor" : "/srv/airtime/stor";
-            $diskUsage = shell_exec("du -sb $storDir | awk '{print $1}'");
-        
-            Application_Model_Preference::setDiskUsage($diskUsage);
-                    
-            //clear out the cache
-            Cache::clear();
-            
-            $con->commit();
-        
-            //update system_version in cc_pref and change some columns in cc_files
-            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
-            $values = parse_ini_file($airtimeConf, true);
-        
-            $username = $values['database']['dbuser'];
-            $password = $values['database']['dbpass'];
-            $host = $values['database']['host'];
-            $database = $values['database']['dbname'];
-        
-            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_".$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
-            Application_Model_Preference::SetSchemaVersion($this->getNewVersion());
 
-            //clear out the cache
-            Cache::clear();
-            
-            $this->toggleMaintenanceScreen(false);
-                    
-        } catch (Exception $e) {
-            $con->rollback();
-            $this->toggleMaintenanceScreen(false);
-        }        
+    protected function _runUpgrade()
+    {
+        //Update disk_usage value in cc_pref
+        $storDir = isset($_SERVER['AIRTIME_BASE']) ? $_SERVER['AIRTIME_BASE']."srv/airtime/stor" : "/srv/airtime/stor";
+        $diskUsage = shell_exec("du -sb $storDir | awk '{print $1}'");
+
+        Application_Model_Preference::setDiskUsage($diskUsage);
+
+        //update system_version in cc_pref and change some columns in cc_files
+        parent::_runUpgrade();
     }
 }
 
@@ -205,78 +207,49 @@ class AirtimeUpgrader254 extends AirtimeUpgrader
         return '2.5.4';
     }
     
-    public function upgrade()
+    protected function _runUpgrade()
     {
-        Cache::clear();
-        
-        assert($this->checkIfUpgradeSupported());
-        
-        $newVersion = $this->getNewVersion();
-        
-        $con = Propel::getConnection();
-        //$con->beginTransaction();
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-            
-            //Begin upgrade
-
-            //First, ensure there are no superadmins already.
-            $numberOfSuperAdmins = CcSubjsQuery::create()
+        //First, ensure there are no superadmins already.
+        $numberOfSuperAdmins = CcSubjsQuery::create()
             ->filterByDbType(UTYPE_SUPERADMIN)
             ->filterByDbLogin("sourcefabric_admin", Criteria::NOT_EQUAL) //Ignore sourcefabric_admin users
             ->count();
-            
-            //Only create a super admin if there isn't one already.
-            if ($numberOfSuperAdmins == 0)
-            {
-                //Find the "admin" user and promote them to superadmin.
-                $adminUser = CcSubjsQuery::create()
+
+        //Only create a super admin if there isn't one already.
+        if ($numberOfSuperAdmins == 0)
+        {
+            //Find the "admin" user and promote them to superadmin.
+            $adminUser = CcSubjsQuery::create()
                 ->filterByDbLogin('admin')
                 ->findOne();
-                if (!$adminUser)
-                {
-                    //TODO: Otherwise get the user with the lowest ID that is of type administrator:
-                    //
-                    $adminUser = CcSubjsQuery::create()
+            if (!$adminUser)
+            {
+                // Otherwise get the user with the lowest ID that is of type administrator:
+                $adminUser = CcSubjsQuery::create()
                     ->filterByDbType(UTYPE_ADMIN)
                     ->orderByDbId(Criteria::ASC)
                     ->findOne();
-                    
-                    if (!$adminUser) {
-                        throw new Exception("Failed to find any users of type 'admin' ('A').");
-                    }
-                }
-                
-                $adminUser = new Application_Model_User($adminUser->getDbId());
-                $adminUser->setType(UTYPE_SUPERADMIN);
-                $adminUser->save();
-                Logging::info($_SERVER['HTTP_HOST'] . ': ' . $newVersion . " Upgrade: Promoted user " . $adminUser->getLogin() . " to be a Super Admin.");
-                
-                //Also try to promote the sourcefabric_admin user
-                $sofabAdminUser = CcSubjsQuery::create()
-                ->filterByDbLogin('sourcefabric_admin')
-                ->findOne();
-                if ($sofabAdminUser) {
-                    $sofabAdminUser = new Application_Model_User($sofabAdminUser->getDbId());
-                    $sofabAdminUser->setType(UTYPE_SUPERADMIN);
-                    $sofabAdminUser->save();
-                    Logging::info($_SERVER['HTTP_HOST'] . ': ' . $newVersion . " Upgrade: Promoted user " . $sofabAdminUser->getLogin() . " to be a Super Admin.");                  
+
+                if (!$adminUser) {
+                    throw new Exception("Failed to find any users of type 'admin' ('A').");
                 }
             }
-            
-            //$con->commit();
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-            
-            $this->toggleMaintenanceScreen(false);
-                        
-            return true;
-            
-        } catch(Exception $e) {
-            //$con->rollback();
-            $this->toggleMaintenanceScreen(false);
-            throw $e; 
+
+            $adminUser = new Application_Model_User($adminUser->getDbId());
+            $adminUser->setType(UTYPE_SUPERADMIN);
+            $adminUser->save();
+            Logging::info($_SERVER['HTTP_HOST'] . ': ' . $this->getNewVersion() . " Upgrade: Promoted user " . $adminUser->getLogin() . " to be a Super Admin.");
+
+            //Also try to promote the sourcefabric_admin user
+            $sofabAdminUser = CcSubjsQuery::create()
+                ->filterByDbLogin('sourcefabric_admin')
+                ->findOne();
+            if ($sofabAdminUser) {
+                $sofabAdminUser = new Application_Model_User($sofabAdminUser->getDbId());
+                $sofabAdminUser->setType(UTYPE_SUPERADMIN);
+                $sofabAdminUser->save();
+                Logging::info($_SERVER['HTTP_HOST'] . ': ' . $this->getNewVersion() . " Upgrade: Promoted user " . $sofabAdminUser->getLogin() . " to be a Super Admin.");
+            }
         }
     }
 }
@@ -291,40 +264,6 @@ class AirtimeUpgrader255 extends AirtimeUpgrader {
     public function getNewVersion() {
         return '2.5.5';
     }
-
-    public function upgrade($dir = __DIR__) {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-
-        $newVersion = $this->getNewVersion();
-
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-            
-            // Begin upgrade
-            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
-            $values = parse_ini_file($airtimeConf, true);
-            
-            $username = $values['database']['dbuser'];
-            $password = $values['database']['dbpass'];
-            $host = $values['database']['host'];
-            $database = $values['database']['dbname'];
-
-            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
-                    .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
-            
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-            
-            $this->toggleMaintenanceScreen(false);
-            
-            return true;
-        } catch(Exception $e) {
-            $this->toggleMaintenanceScreen(false);
-            throw $e;
-        }
-    }
 }
 
 class AirtimeUpgrader259 extends AirtimeUpgrader {
@@ -337,38 +276,6 @@ class AirtimeUpgrader259 extends AirtimeUpgrader {
     public function getNewVersion() {
         return '2.5.9';
     }
-    
-    public function upgrade($dir = __DIR__) {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-        
-        $newVersion = $this->getNewVersion();
-        
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-            
-            // Begin upgrade
-            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
-            $values = parse_ini_file($airtimeConf, true);
-            
-            $username = $values['database']['dbuser'];
-            $password = $values['database']['dbpass'];
-            $host = $values['database']['host'];
-            $database = $values['database']['dbname'];
-                
-            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
-                     .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
-            
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-            
-            $this->toggleMaintenanceScreen(false);
-        } catch(Exception $e) {
-            $this->toggleMaintenanceScreen(false);
-            throw $e;
-        }
-    }
 }
 
 class AirtimeUpgrader2510 extends AirtimeUpgrader
@@ -382,38 +289,6 @@ class AirtimeUpgrader2510 extends AirtimeUpgrader
     public function getNewVersion() {
         return '2.5.10';
     }
-
-    public function upgrade($dir = __DIR__) {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-
-        $newVersion = $this->getNewVersion();
-
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-
-            // Begin upgrade
-            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
-            $values = parse_ini_file($airtimeConf, true);
-
-            $username = $values['database']['dbuser'];
-            $password = $values['database']['dbpass'];
-            $host = $values['database']['host'];
-            $database = $values['database']['dbname'];
-
-            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
-                .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
-
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-
-            $this->toggleMaintenanceScreen(false);
-        } catch(Exception $e) {
-            $this->toggleMaintenanceScreen(false);
-            throw $e;
-        }
-    }
 }
 
 class AirtimeUpgrader2511 extends AirtimeUpgrader
@@ -428,35 +303,13 @@ class AirtimeUpgrader2511 extends AirtimeUpgrader
         return '2.5.11';
     }
 
-    public function upgrade($dir = __DIR__) {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-
-        $newVersion = $this->getNewVersion();
-
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-
-            // Begin upgrade
-            $queryResult = CcFilesQuery::create()
-                ->select(array('disk_usage'))
-                ->withColumn('SUM(CcFiles.filesize)', 'disk_usage')
-                ->find();
-            $disk_usage = $queryResult[0];
-            Application_Model_Preference::setDiskUsage($disk_usage);
-
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-
-            $this->toggleMaintenanceScreen(false);
-        } catch(Exception $e) {
-            $this->toggleMaintenanceScreen(false);
-            throw $e;
-        }
-    }
-    public function downgrade() {
-
+    protected function _runUpgrade() {
+        $queryResult = CcFilesQuery::create()
+            ->select(array('disk_usage'))
+            ->withColumn('SUM(CcFiles.filesize)', 'disk_usage')
+            ->find();
+        $disk_usage = $queryResult[0];
+        Application_Model_Preference::setDiskUsage($disk_usage);
     }
 }
 
@@ -472,41 +325,6 @@ class AirtimeUpgrader2512 extends AirtimeUpgrader
     public function getNewVersion() {
         return '2.5.12';
     }
-
-    public function upgrade($dir = __DIR__) {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-
-        $newVersion = $this->getNewVersion();
-
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-
-            // Begin upgrade
-            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
-            $values = parse_ini_file($airtimeConf, true);
-
-            $username = $values['database']['dbuser'];
-            $password = $values['database']['dbpass'];
-            $host = $values['database']['host'];
-            $database = $values['database']['dbname'];
-
-            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
-                .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
-
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-
-            $this->toggleMaintenanceScreen(false);
-        } catch(Exception $e) {
-            $this->toggleMaintenanceScreen(false);
-            throw $e;
-        }
-    }
-    public function downgrade() {
-
-    }
 }
 
 class AirtimeUpgrader2513 extends AirtimeUpgrader
@@ -520,38 +338,4 @@ class AirtimeUpgrader2513 extends AirtimeUpgrader
     public function getNewVersion() {
         return '2.5.13';
     }
-
-    public function upgrade($dir = __DIR__) {
-        Cache::clear();
-        assert($this->checkIfUpgradeSupported());
-
-        $newVersion = $this->getNewVersion();
-
-        try {
-            $this->toggleMaintenanceScreen(true);
-            Cache::clear();
-
-            // Begin upgrade
-            $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
-            $values = parse_ini_file($airtimeConf, true);
-
-            $username = $values['database']['dbuser'];
-            $password = $values['database']['dbpass'];
-            $host = $values['database']['host'];
-            $database = $values['database']['dbname'];
-
-            passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/upgrade_sql/airtime_"
-                     .$newVersion."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
-
-            Application_Model_Preference::SetSchemaVersion($newVersion);
-            Cache::clear();
-
-            $this->toggleMaintenanceScreen(false);
-        } catch(Exception $e) {
-            $this->toggleMaintenanceScreen(false);
-            throw $e;
-        }
-    }
-
 }
-
diff --git a/airtime_mvc/tests/airtime.conf b/airtime_mvc/tests/airtime.conf
index fb19bcaa0..431d3f2c2 100644
--- a/airtime_mvc/tests/airtime.conf
+++ b/airtime_mvc/tests/airtime.conf
@@ -19,6 +19,7 @@ base_url = localhost
 base_port = 80
 base_dir = /
 cache_ahead_hours = 1
+station_id = teststation
 
 [monit]
 monit_user = guest
diff --git a/python_apps/airtime-celery/README.rst b/python_apps/airtime-celery/README.rst
new file mode 100644
index 000000000..7de00cf7b
--- /dev/null
+++ b/python_apps/airtime-celery/README.rst
@@ -0,0 +1,49 @@
+airtime-celery
+==============
+
+airtime-celery is a Celery_ daemon for handling backend tasks asynchronously.
+Communication and the Celery results backend are both handled with amqp (RabbitMQ).
+
+Installation
+============
+
+    $ sudo python setup.py install
+
+To install the configuration and upstart files for all environments (development and production)
+
+    $ sudo python setup.py install --all-envs
+
+You can also specify a single environment to deploy config and upstart files for:
+
+    $ sudo python setup.py install --dev-env=bananas
+
+Each instance of airtime-celery has its own worker, and multiple instances can be run in parallel.
+`Celery is thread-safe`_, so this parallelization won't cause conflicts.
+
+.. _Celery: http://www.celeryproject.org/
+.. _Celery is thread-safe: http://celery.readthedocs.org/en/latest/userguide/application.html
+
+Usage
+=====
+
+This program must be run with sudo:
+
+    $ sudo service airtime-celery {start | stop | restart | graceful | kill | dryrun | create-paths}
+
+Developers
+==========
+
+You may want to use the setuptools develop target to install:
+
+    $ sudo python setup.py develop
+
+You will need to allow the "airtime" RabbitMQ user to access all exchanges and queues within the /airtime vhost:
+
+    $ sudo rabbitmqctl set_permissions -p /airtime airtime .\* .\* .\*
+
+Logging
+=======
+
+By default, logs are saved to:
+
+    /var/log/airtime/airtime-celery[-DEV_ENV].log
diff --git a/python_apps/airtime-celery/airtime-celery/celeryconfig.py b/python_apps/airtime-celery/airtime-celery/celeryconfig.py
index fcdd83a70..cb986d0f9 100644
--- a/python_apps/airtime-celery/airtime-celery/celeryconfig.py
+++ b/python_apps/airtime-celery/airtime-celery/celeryconfig.py
@@ -1,26 +1,26 @@
+import os
 from configobj import ConfigObj
 from kombu import Exchange, Queue
 
 # Get the broker string from airtime.conf
-DEFAULT_RMQ_CONFIG_PATH = '/etc/airtime/airtime.conf'
 RMQ_CONFIG_SECTION = "rabbitmq"
 
 
+def get_rmq_broker():
+    rmq_config = ConfigObj(os.environ['RMQ_CONFIG_FILE'])
+    rmq_settings = parse_rmq_config(rmq_config)
+    return 'amqp://{username}:{password}@{host}:{port}/{vhost}'.format(**rmq_settings)
+
+
 def parse_rmq_config(rmq_config):
     return {
-        'host'     : rmq_config[RMQ_CONFIG_SECTION]['host'],
-        'port'     : rmq_config[RMQ_CONFIG_SECTION]['port'],
-        'username' : rmq_config[RMQ_CONFIG_SECTION]['user'],
-        'password' : rmq_config[RMQ_CONFIG_SECTION]['password'],
-        'vhost'    : rmq_config[RMQ_CONFIG_SECTION]['vhost']
+        'host'    : rmq_config[RMQ_CONFIG_SECTION]['host'],
+        'port'    : rmq_config[RMQ_CONFIG_SECTION]['port'],
+        'username': rmq_config[RMQ_CONFIG_SECTION]['user'],
+        'password': rmq_config[RMQ_CONFIG_SECTION]['password'],
+        'vhost'   : rmq_config[RMQ_CONFIG_SECTION]['vhost']
     }
 
-
-def get_rmq_broker():
-    rmq_config = ConfigObj(DEFAULT_RMQ_CONFIG_PATH)
-    rmq_settings = parse_rmq_config(rmq_config)
-    return 'amqp://{username}:{password}@{host}:{port}/{vhost}'.format(**rmq_settings)
-
 # Celery amqp settings
 BROKER_URL = get_rmq_broker()
 CELERY_RESULT_BACKEND = 'amqp'     # Use RabbitMQ as the celery backend
@@ -34,7 +34,7 @@ CELERY_QUEUES = (
 )
 CELERY_ROUTES = (
     {
-        'soundcloud_uploads.uploader.upload_to_soundcloud': {
+        'soundcloud_uploads.tasks.upload_to_soundcloud': {
             'exchange': 'airtime-results',
             'queue': 'airtime-results.soundcloud-uploads',
         }
diff --git a/python_apps/airtime-celery/airtime-celery/uploader.py b/python_apps/airtime-celery/airtime-celery/tasks.py
similarity index 73%
rename from python_apps/airtime-celery/airtime-celery/uploader.py
rename to python_apps/airtime-celery/airtime-celery/tasks.py
index addbc964d..76009a75f 100644
--- a/python_apps/airtime-celery/airtime-celery/uploader.py
+++ b/python_apps/airtime-celery/airtime-celery/tasks.py
@@ -11,6 +11,16 @@ logger = get_task_logger(__name__)
 
 @celery.task(name='upload-to-soundcloud')
 def upload_to_soundcloud(data, token, file_path):
+    """
+    Upload a file to SoundCloud
+
+    :param data:      associative array containing SoundCloud metadata
+    :param token:     OAuth2 client access token
+    :param file_path: path to the file being uploaded
+
+    :return: the SoundCloud response object
+    :rtype: dict
+    """
     client = soundcloud.Client(access_token=token)
     # Open the file with urllib2 if it's a cloud file
     data['asset_data'] = open(file_path, 'rb') if os.path.isfile(file_path) else urllib2.urlopen(file_path)
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery b/python_apps/airtime-celery/install/conf/airtime-celery
index b026a8069..3b1206738 100644
--- a/python_apps/airtime-celery/install/conf/airtime-celery
+++ b/python_apps/airtime-celery/install/conf/airtime-celery
@@ -5,10 +5,10 @@ CELERYD_NODES="airtime-celery"
 CELERY_BIN="/usr/local/bin/celery"
 
 # App instance to use
-CELERY_APP="airtime-celery.uploader:celery"
+CELERY_APP="airtime-celery.tasks:celery"
 
 # Extra command-line arguments to the worker
-CELERYD_OPTS="--time-limit=300 --concurrency=8 --config=celeryconfig"
+CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
 
 # %N will be replaced with the first part of the nodename.
 CELERYD_LOG_FILE="/var/log/airtime/%N.log"
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-bananas b/python_apps/airtime-celery/install/conf/airtime-celery-bananas
new file mode 100644
index 000000000..15cd5d8b3
--- /dev/null
+++ b/python_apps/airtime-celery/install/conf/airtime-celery-bananas
@@ -0,0 +1,25 @@
+# Names of nodes to start
+CELERYD_NODES="airtime-celery-bananas"
+
+# Absolute or relative path to the 'celery' command:
+CELERY_BIN="/usr/local/bin/celery"
+
+# App instance to use
+CELERY_APP="airtime-celery.tasks:celery"
+
+# Extra command-line arguments to the worker
+CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
+
+# %N will be replaced with the first part of the nodename.
+CELERYD_LOG_FILE="/var/log/airtime/%N.log"
+CELERYD_PID_FILE="/var/run/celery/%N.pid"
+
+# Workers should run as an unprivileged user.
+#   You need to create this user manually (or you can choose
+#   a user/group combination that already exists, e.g. nobody).
+CELERYD_USER="celery"
+CELERYD_GROUP="celery"
+
+# If enabled pid and log directories will be created if missing,
+# and owned by the userid/group configured.
+CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-cliff b/python_apps/airtime-celery/install/conf/airtime-celery-cliff
new file mode 100644
index 000000000..a08e14bde
--- /dev/null
+++ b/python_apps/airtime-celery/install/conf/airtime-celery-cliff
@@ -0,0 +1,25 @@
+# Names of nodes to start
+CELERYD_NODES="airtime-celery-cliff"
+
+# Absolute or relative path to the 'celery' command:
+CELERY_BIN="/usr/local/bin/celery"
+
+# App instance to use
+CELERY_APP="airtime-celery.tasks:celery"
+
+# Extra command-line arguments to the worker
+CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
+
+# %N will be replaced with the first part of the nodename.
+CELERYD_LOG_FILE="/var/log/airtime/%N.log"
+CELERYD_PID_FILE="/var/run/celery/%N.pid"
+
+# Workers should run as an unprivileged user.
+#   You need to create this user manually (or you can choose
+#   a user/group combination that already exists, e.g. nobody).
+CELERYD_USER="celery"
+CELERYD_GROUP="celery"
+
+# If enabled pid and log directories will be created if missing,
+# and owned by the userid/group configured.
+CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-production b/python_apps/airtime-celery/install/conf/airtime-celery-production
new file mode 100644
index 000000000..69f8e6b7a
--- /dev/null
+++ b/python_apps/airtime-celery/install/conf/airtime-celery-production
@@ -0,0 +1,25 @@
+# Names of nodes to start
+CELERYD_NODES="airtime-celery-production"
+
+# Absolute or relative path to the 'celery' command:
+CELERY_BIN="/usr/local/bin/celery"
+
+# App instance to use
+CELERY_APP="airtime-celery.tasks:celery"
+
+# Extra command-line arguments to the worker
+CELERYD_OPTS="--time-limit=300 --concurrency=8 --config=celeryconfig"
+
+# %N will be replaced with the first part of the nodename.
+CELERYD_LOG_FILE="/var/log/airtime/%N.log"
+CELERYD_PID_FILE="/var/run/celery/%N.pid"
+
+# Workers should run as an unprivileged user.
+#   You need to create this user manually (or you can choose
+#   a user/group combination that already exists, e.g. nobody).
+CELERYD_USER="celery"
+CELERYD_GROUP="celery"
+
+# If enabled pid and log directories will be created if missing,
+# and owned by the userid/group configured.
+CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-staging b/python_apps/airtime-celery/install/conf/airtime-celery-staging
new file mode 100644
index 000000000..e85252291
--- /dev/null
+++ b/python_apps/airtime-celery/install/conf/airtime-celery-staging
@@ -0,0 +1,25 @@
+# Names of nodes to start
+CELERYD_NODES="airtime-celery-staging"
+
+# Absolute or relative path to the 'celery' command:
+CELERY_BIN="/usr/local/bin/celery"
+
+# App instance to use
+CELERY_APP="airtime-celery.tasks:celery"
+
+# Extra command-line arguments to the worker
+CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
+
+# %N will be replaced with the first part of the nodename.
+CELERYD_LOG_FILE="/var/log/airtime/%N.log"
+CELERYD_PID_FILE="/var/run/celery/%N.pid"
+
+# Workers should run as an unprivileged user.
+#   You need to create this user manually (or you can choose
+#   a user/group combination that already exists, e.g. nobody).
+CELERYD_USER="celery"
+CELERYD_GROUP="celery"
+
+# If enabled pid and log directories will be created if missing,
+# and owned by the userid/group configured.
+CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/upstart/airtime-celery b/python_apps/airtime-celery/install/initd/airtime-celery
similarity index 99%
rename from python_apps/airtime-celery/install/upstart/airtime-celery
rename to python_apps/airtime-celery/install/initd/airtime-celery
index 34e990ab8..d4f6ae89e 100644
--- a/python_apps/airtime-celery/install/upstart/airtime-celery
+++ b/python_apps/airtime-celery/install/initd/airtime-celery
@@ -37,6 +37,7 @@ if [ $(id -u) -ne 0 ]; then
     exit 1
 fi
 
+export RMQ_CONFIG_FILE="/etc/airtime/airtime.conf"
 
 # Can be a runlevel symlink (e.g. S02celeryd)
 if [ -L "$0" ]; then
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-bananas b/python_apps/airtime-celery/install/initd/airtime-celery-bananas
new file mode 100644
index 000000000..c91c36713
--- /dev/null
+++ b/python_apps/airtime-celery/install/initd/airtime-celery-bananas
@@ -0,0 +1,334 @@
+#!/bin/sh -e
+# ============================================
+#  celeryd - Starts the Celery worker daemon.
+# ============================================
+#
+# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
+# :Configuration file: /etc/default/celeryd
+#
+# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
+
+
+### BEGIN INIT INFO
+# Provides:          celeryd
+# Required-Start:    $network $local_fs $remote_fs
+# Required-Stop:     $network $local_fs $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: celery task worker daemon
+### END INIT INFO
+#
+#
+# To implement separate init scripts, copy this script and give it a different
+# name:
+# I.e., if my new application, "little-worker" needs an init, I
+# should just use:
+#
+#   cp /etc/init.d/celeryd /etc/init.d/little-worker
+#
+# You can then configure this by manipulating /etc/default/little-worker.
+#
+VERSION=10.1
+echo "celery init v${VERSION}."
+if [ $(id -u) -ne 0 ]; then
+    echo "Error: This program can only be used by the root user."
+    echo "       Unprivileged users must use the 'celery multi' utility, "
+    echo "       or 'celery worker --detach'."
+    exit 1
+fi
+
+export RMQ_CONFIG_FILE="/etc/airtime-saas/bananas/rabbitmq-analyzer.ini"
+
+# Can be a runlevel symlink (e.g. S02celeryd)
+if [ -L "$0" ]; then
+    SCRIPT_FILE=$(readlink "$0")
+else
+    SCRIPT_FILE="$0"
+fi
+SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
+
+DEFAULT_USER="celery"
+DEFAULT_PID_FILE="/var/run/celery/%n.pid"
+DEFAULT_LOG_FILE="/var/log/celery/%n.log"
+DEFAULT_LOG_LEVEL="INFO"
+DEFAULT_NODES="celery"
+DEFAULT_CELERYD="-m celery worker --detach"
+
+CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
+# Make sure executable configuration script is owned by root
+_config_sanity() {
+    local path="$1"
+    local owner=$(ls -ld "$path" | awk '{print $3}')
+    local iwgrp=$(ls -ld "$path" | cut -b 6)
+    local iwoth=$(ls -ld "$path" | cut -b 9)
+    if [ "$(id -u $owner)" != "0" ]; then
+        echo "Error: Config script '$path' must be owned by root!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with mailicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change ownership of the script:"
+        echo "    $ sudo chown root '$path'"
+        exit 1
+    fi
+    if [ "$iwoth" != "-" ]; then  # S_IWOTH
+        echo "Error: Config script '$path' cannot be writable by others!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
+        echo "Error: Config script '$path' cannot be writable by group!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+}
+if [ -f "$CELERY_DEFAULTS" ]; then
+    _config_sanity "$CELERY_DEFAULTS"
+    echo "Using config script: $CELERY_DEFAULTS"
+    . "$CELERY_DEFAULTS"
+fi
+# Sets --app argument for CELERY_BIN
+CELERY_APP_ARG=""
+if [ ! -z "$CELERY_APP" ]; then
+    CELERY_APP_ARG="--app=$CELERY_APP"
+fi
+CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
+# Set CELERY_CREATE_DIRS to always create log/pid dirs.
+CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
+CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
+CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
+if [ -z "$CELERYD_PID_FILE" ]; then
+    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
+    CELERY_CREATE_RUNDIR=1
+fi
+if [ -z "$CELERYD_LOG_FILE" ]; then
+    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
+    CELERY_CREATE_LOGDIR=1
+fi
+CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
+CELERY_BIN=${CELERY_BIN:-"celery"}
+CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
+CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
+export CELERY_LOADER
+if [ -n "$2" ]; then
+    CELERYD_OPTS="$CELERYD_OPTS $2"
+fi
+CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
+CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
+# Extra start-stop-daemon options, like user/group.
+if [ -n "$CELERYD_CHDIR" ]; then
+    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
+fi
+check_dev_null() {
+    if [ ! -c /dev/null ]; then
+        echo "/dev/null is not a character device!"
+        exit 75  # EX_TEMPFAIL
+    fi
+}
+maybe_die() {
+    if [ $? -ne 0 ]; then
+        echo "Exiting: $* (errno $?)"
+        exit 77  # EX_NOPERM
+    fi
+}
+create_default_dir() {
+    if [ ! -d "$1" ]; then
+        echo "- Creating default directory: '$1'"
+        mkdir -p "$1"
+        maybe_die "Couldn't create directory $1"
+        echo "- Changing permissions of '$1' to 02755"
+        chmod 02755 "$1"
+        maybe_die "Couldn't change permissions for $1"
+        if [ -n "$CELERYD_USER" ]; then
+            echo "- Changing owner of '$1' to '$CELERYD_USER'"
+            chown "$CELERYD_USER" "$1"
+            maybe_die "Couldn't change owner of $1"
+        fi
+        if [ -n "$CELERYD_GROUP" ]; then
+            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
+            chgrp "$CELERYD_GROUP" "$1"
+            maybe_die "Couldn't change group of $1"
+        fi
+    fi
+}
+check_paths() {
+    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_LOG_DIR"
+    fi
+    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_PID_DIR"
+    fi
+}
+create_paths() {
+    create_default_dir "$CELERYD_LOG_DIR"
+    create_default_dir "$CELERYD_PID_DIR"
+}
+export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
+_get_pidfiles () {
+    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
+    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
+}
+_get_pids() {
+    found_pids=0
+    my_exitcode=0
+    for pidfile in $(_get_pidfiles); do
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+            my_exitcode=1
+        else
+            found_pids=1
+            echo "$pid"
+        fi
+    if [ $found_pids -eq 0 ]; then
+        echo "${SCRIPT_NAME}: All nodes down"
+        exit $my_exitcode
+    fi
+    done
+}
+_chuid () {
+    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
+}
+start_workers () {
+    if [ ! -z "$CELERYD_ULIMIT" ]; then
+        ulimit $CELERYD_ULIMIT
+    fi
+    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
+                 --pidfile="$CELERYD_PID_FILE"      \
+                 --logfile="$CELERYD_LOG_FILE"      \
+                 --loglevel="$CELERYD_LOG_LEVEL"    \
+                 $CELERY_APP_ARG                    \
+                 $CELERYD_OPTS
+}
+dryrun () {
+    (C_FAKEFORK=1 start_workers --verbose)
+}
+stop_workers () {
+    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers () {
+    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
+                   --pidfile="$CELERYD_PID_FILE"    \
+                   --logfile="$CELERYD_LOG_FILE"    \
+                   --loglevel="$CELERYD_LOG_LEVEL"  \
+                   $CELERY_APP_ARG                  \
+                   $CELERYD_OPTS
+}
+kill_workers() {
+    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers_graceful () {
+    echo "WARNING: Use with caution in production"
+    echo "The workers will attempt to restart, but they may not be able to."
+    local worker_pids=
+    worker_pids=`_get_pids`
+    [ "$one_failed" ] && exit 1
+    for worker_pid in $worker_pids; do
+        local failed=
+        kill -HUP $worker_pid 2> /dev/null || failed=true
+        if [ "$failed" ]; then
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
+            one_failed=true
+        else
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+check_status () {
+    my_exitcode=0
+    found_pids=0
+    local one_failed=
+    for pidfile in $(_get_pidfiles); do
+        if [ ! -r $pidfile ]; then
+            echo "${SCRIPT_NAME} down: no pidfiles found"
+            one_failed=true
+            break
+        fi
+        local node=`basename "$pidfile" .pid`
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+        else
+            local failed=
+            kill -0 $pid 2> /dev/null || failed=true
+            if [ "$failed" ]; then
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
+                one_failed=true
+            else
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
+            fi
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+case "$1" in
+    start)
+        check_dev_null
+        check_paths
+        start_workers
+    ;;
+    stop)
+        check_dev_null
+        check_paths
+        stop_workers
+    ;;
+    reload|force-reload)
+        echo "Use restart"
+    ;;
+    status)
+        check_status
+    ;;
+    restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    graceful)
+        check_dev_null
+        restart_workers_graceful
+    ;;
+    kill)
+        check_dev_null
+        kill_workers
+    ;;
+    dryrun)
+        check_dev_null
+        dryrun
+    ;;
+    try-restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    create-paths)
+        check_dev_null
+        create_paths
+    ;;
+    check-paths)
+        check_dev_null
+        check_paths
+    ;;
+    *)
+        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
+        exit 64  # EX_USAGE
+    ;;
+esac
+exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-cliff b/python_apps/airtime-celery/install/initd/airtime-celery-cliff
new file mode 100644
index 000000000..a2d0291a8
--- /dev/null
+++ b/python_apps/airtime-celery/install/initd/airtime-celery-cliff
@@ -0,0 +1,334 @@
+#!/bin/sh -e
+# ============================================
+#  celeryd - Starts the Celery worker daemon.
+# ============================================
+#
+# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
+# :Configuration file: /etc/default/celeryd
+#
+# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
+
+
+### BEGIN INIT INFO
+# Provides:          celeryd
+# Required-Start:    $network $local_fs $remote_fs
+# Required-Stop:     $network $local_fs $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: celery task worker daemon
+### END INIT INFO
+#
+#
+# To implement separate init scripts, copy this script and give it a different
+# name:
+# I.e., if my new application, "little-worker" needs an init, I
+# should just use:
+#
+#   cp /etc/init.d/celeryd /etc/init.d/little-worker
+#
+# You can then configure this by manipulating /etc/default/little-worker.
+#
+VERSION=10.1
+echo "celery init v${VERSION}."
+if [ $(id -u) -ne 0 ]; then
+    echo "Error: This program can only be used by the root user."
+    echo "       Unprivileged users must use the 'celery multi' utility, "
+    echo "       or 'celery worker --detach'."
+    exit 1
+fi
+
+export RMQ_CONFIG_FILE="/etc/airtime-saas/cliff/rabbitmq-analyzer.ini"
+
+# Can be a runlevel symlink (e.g. S02celeryd)
+if [ -L "$0" ]; then
+    SCRIPT_FILE=$(readlink "$0")
+else
+    SCRIPT_FILE="$0"
+fi
+SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
+
+DEFAULT_USER="celery"
+DEFAULT_PID_FILE="/var/run/celery/%n.pid"
+DEFAULT_LOG_FILE="/var/log/celery/%n.log"
+DEFAULT_LOG_LEVEL="INFO"
+DEFAULT_NODES="celery"
+DEFAULT_CELERYD="-m celery worker --detach"
+
+CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
+# Make sure executable configuration script is owned by root
+_config_sanity() {
+    local path="$1"
+    local owner=$(ls -ld "$path" | awk '{print $3}')
+    local iwgrp=$(ls -ld "$path" | cut -b 6)
+    local iwoth=$(ls -ld "$path" | cut -b 9)
+    if [ "$(id -u $owner)" != "0" ]; then
+        echo "Error: Config script '$path' must be owned by root!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with mailicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change ownership of the script:"
+        echo "    $ sudo chown root '$path'"
+        exit 1
+    fi
+    if [ "$iwoth" != "-" ]; then  # S_IWOTH
+        echo "Error: Config script '$path' cannot be writable by others!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
+        echo "Error: Config script '$path' cannot be writable by group!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+}
+if [ -f "$CELERY_DEFAULTS" ]; then
+    _config_sanity "$CELERY_DEFAULTS"
+    echo "Using config script: $CELERY_DEFAULTS"
+    . "$CELERY_DEFAULTS"
+fi
+# Sets --app argument for CELERY_BIN
+CELERY_APP_ARG=""
+if [ ! -z "$CELERY_APP" ]; then
+    CELERY_APP_ARG="--app=$CELERY_APP"
+fi
+CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
+# Set CELERY_CREATE_DIRS to always create log/pid dirs.
+CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
+CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
+CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
+if [ -z "$CELERYD_PID_FILE" ]; then
+    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
+    CELERY_CREATE_RUNDIR=1
+fi
+if [ -z "$CELERYD_LOG_FILE" ]; then
+    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
+    CELERY_CREATE_LOGDIR=1
+fi
+CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
+CELERY_BIN=${CELERY_BIN:-"celery"}
+CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
+CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
+export CELERY_LOADER
+if [ -n "$2" ]; then
+    CELERYD_OPTS="$CELERYD_OPTS $2"
+fi
+CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
+CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
+# Extra start-stop-daemon options, like user/group.
+if [ -n "$CELERYD_CHDIR" ]; then
+    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
+fi
+check_dev_null() {
+    if [ ! -c /dev/null ]; then
+        echo "/dev/null is not a character device!"
+        exit 75  # EX_TEMPFAIL
+    fi
+}
+maybe_die() {
+    if [ $? -ne 0 ]; then
+        echo "Exiting: $* (errno $?)"
+        exit 77  # EX_NOPERM
+    fi
+}
+create_default_dir() {
+    if [ ! -d "$1" ]; then
+        echo "- Creating default directory: '$1'"
+        mkdir -p "$1"
+        maybe_die "Couldn't create directory $1"
+        echo "- Changing permissions of '$1' to 02755"
+        chmod 02755 "$1"
+        maybe_die "Couldn't change permissions for $1"
+        if [ -n "$CELERYD_USER" ]; then
+            echo "- Changing owner of '$1' to '$CELERYD_USER'"
+            chown "$CELERYD_USER" "$1"
+            maybe_die "Couldn't change owner of $1"
+        fi
+        if [ -n "$CELERYD_GROUP" ]; then
+            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
+            chgrp "$CELERYD_GROUP" "$1"
+            maybe_die "Couldn't change group of $1"
+        fi
+    fi
+}
+check_paths() {
+    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_LOG_DIR"
+    fi
+    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_PID_DIR"
+    fi
+}
+create_paths() {
+    create_default_dir "$CELERYD_LOG_DIR"
+    create_default_dir "$CELERYD_PID_DIR"
+}
+export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
+_get_pidfiles () {
+    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
+    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
+}
+_get_pids() {
+    found_pids=0
+    my_exitcode=0
+    for pidfile in $(_get_pidfiles); do
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+            my_exitcode=1
+        else
+            found_pids=1
+            echo "$pid"
+        fi
+    if [ $found_pids -eq 0 ]; then
+        echo "${SCRIPT_NAME}: All nodes down"
+        exit $my_exitcode
+    fi
+    done
+}
+_chuid () {
+    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
+}
+start_workers () {
+    if [ ! -z "$CELERYD_ULIMIT" ]; then
+        ulimit $CELERYD_ULIMIT
+    fi
+    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
+                 --pidfile="$CELERYD_PID_FILE"      \
+                 --logfile="$CELERYD_LOG_FILE"      \
+                 --loglevel="$CELERYD_LOG_LEVEL"    \
+                 $CELERY_APP_ARG                    \
+                 $CELERYD_OPTS
+}
+dryrun () {
+    (C_FAKEFORK=1 start_workers --verbose)
+}
+stop_workers () {
+    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers () {
+    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
+                   --pidfile="$CELERYD_PID_FILE"    \
+                   --logfile="$CELERYD_LOG_FILE"    \
+                   --loglevel="$CELERYD_LOG_LEVEL"  \
+                   $CELERY_APP_ARG                  \
+                   $CELERYD_OPTS
+}
+kill_workers() {
+    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers_graceful () {
+    echo "WARNING: Use with caution in production"
+    echo "The workers will attempt to restart, but they may not be able to."
+    local worker_pids=
+    worker_pids=`_get_pids`
+    [ "$one_failed" ] && exit 1
+    for worker_pid in $worker_pids; do
+        local failed=
+        kill -HUP $worker_pid 2> /dev/null || failed=true
+        if [ "$failed" ]; then
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
+            one_failed=true
+        else
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+check_status () {
+    my_exitcode=0
+    found_pids=0
+    local one_failed=
+    for pidfile in $(_get_pidfiles); do
+        if [ ! -r $pidfile ]; then
+            echo "${SCRIPT_NAME} down: no pidfiles found"
+            one_failed=true
+            break
+        fi
+        local node=`basename "$pidfile" .pid`
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+        else
+            local failed=
+            kill -0 $pid 2> /dev/null || failed=true
+            if [ "$failed" ]; then
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
+                one_failed=true
+            else
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
+            fi
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+case "$1" in
+    start)
+        check_dev_null
+        check_paths
+        start_workers
+    ;;
+    stop)
+        check_dev_null
+        check_paths
+        stop_workers
+    ;;
+    reload|force-reload)
+        echo "Use restart"
+    ;;
+    status)
+        check_status
+    ;;
+    restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    graceful)
+        check_dev_null
+        restart_workers_graceful
+    ;;
+    kill)
+        check_dev_null
+        kill_workers
+    ;;
+    dryrun)
+        check_dev_null
+        dryrun
+    ;;
+    try-restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    create-paths)
+        check_dev_null
+        create_paths
+    ;;
+    check-paths)
+        check_dev_null
+        check_paths
+    ;;
+    *)
+        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
+        exit 64  # EX_USAGE
+    ;;
+esac
+exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-production b/python_apps/airtime-celery/install/initd/airtime-celery-production
new file mode 100644
index 000000000..43f98d2d5
--- /dev/null
+++ b/python_apps/airtime-celery/install/initd/airtime-celery-production
@@ -0,0 +1,334 @@
+#!/bin/sh -e
+# ============================================
+#  celeryd - Starts the Celery worker daemon.
+# ============================================
+#
+# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
+# :Configuration file: /etc/default/celeryd
+#
+# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
+
+
+### BEGIN INIT INFO
+# Provides:          celeryd
+# Required-Start:    $network $local_fs $remote_fs
+# Required-Stop:     $network $local_fs $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: celery task worker daemon
+### END INIT INFO
+#
+#
+# To implement separate init scripts, copy this script and give it a different
+# name:
+# I.e., if my new application, "little-worker" needs an init, I
+# should just use:
+#
+#   cp /etc/init.d/celeryd /etc/init.d/little-worker
+#
+# You can then configure this by manipulating /etc/default/little-worker.
+#
+VERSION=10.1
+echo "celery init v${VERSION}."
+if [ $(id -u) -ne 0 ]; then
+    echo "Error: This program can only be used by the root user."
+    echo "       Unprivileged users must use the 'celery multi' utility, "
+    echo "       or 'celery worker --detach'."
+    exit 1
+fi
+
+export RMQ_CONFIG_FILE="/etc/airtime-saas/production/rabbitmq-analyzer.ini"
+
+# Can be a runlevel symlink (e.g. S02celeryd)
+if [ -L "$0" ]; then
+    SCRIPT_FILE=$(readlink "$0")
+else
+    SCRIPT_FILE="$0"
+fi
+SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
+
+DEFAULT_USER="celery"
+DEFAULT_PID_FILE="/var/run/celery/%n.pid"
+DEFAULT_LOG_FILE="/var/log/celery/%n.log"
+DEFAULT_LOG_LEVEL="INFO"
+DEFAULT_NODES="celery"
+DEFAULT_CELERYD="-m celery worker --detach"
+
+CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
+# Make sure executable configuration script is owned by root
+_config_sanity() {
+    local path="$1"
+    local owner=$(ls -ld "$path" | awk '{print $3}')
+    local iwgrp=$(ls -ld "$path" | cut -b 6)
+    local iwoth=$(ls -ld "$path" | cut -b 9)
+    if [ "$(id -u $owner)" != "0" ]; then
+        echo "Error: Config script '$path' must be owned by root!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with mailicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change ownership of the script:"
+        echo "    $ sudo chown root '$path'"
+        exit 1
+    fi
+    if [ "$iwoth" != "-" ]; then  # S_IWOTH
+        echo "Error: Config script '$path' cannot be writable by others!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
+        echo "Error: Config script '$path' cannot be writable by group!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+}
+if [ -f "$CELERY_DEFAULTS" ]; then
+    _config_sanity "$CELERY_DEFAULTS"
+    echo "Using config script: $CELERY_DEFAULTS"
+    . "$CELERY_DEFAULTS"
+fi
+# Sets --app argument for CELERY_BIN
+CELERY_APP_ARG=""
+if [ ! -z "$CELERY_APP" ]; then
+    CELERY_APP_ARG="--app=$CELERY_APP"
+fi
+CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
+# Set CELERY_CREATE_DIRS to always create log/pid dirs.
+CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
+CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
+CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
+if [ -z "$CELERYD_PID_FILE" ]; then
+    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
+    CELERY_CREATE_RUNDIR=1
+fi
+if [ -z "$CELERYD_LOG_FILE" ]; then
+    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
+    CELERY_CREATE_LOGDIR=1
+fi
+CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
+CELERY_BIN=${CELERY_BIN:-"celery"}
+CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
+CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
+export CELERY_LOADER
+if [ -n "$2" ]; then
+    CELERYD_OPTS="$CELERYD_OPTS $2"
+fi
+CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
+CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
+# Extra start-stop-daemon options, like user/group.
+if [ -n "$CELERYD_CHDIR" ]; then
+    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
+fi
+check_dev_null() {
+    if [ ! -c /dev/null ]; then
+        echo "/dev/null is not a character device!"
+        exit 75  # EX_TEMPFAIL
+    fi
+}
+maybe_die() {
+    if [ $? -ne 0 ]; then
+        echo "Exiting: $* (errno $?)"
+        exit 77  # EX_NOPERM
+    fi
+}
+create_default_dir() {
+    if [ ! -d "$1" ]; then
+        echo "- Creating default directory: '$1'"
+        mkdir -p "$1"
+        maybe_die "Couldn't create directory $1"
+        echo "- Changing permissions of '$1' to 02755"
+        chmod 02755 "$1"
+        maybe_die "Couldn't change permissions for $1"
+        if [ -n "$CELERYD_USER" ]; then
+            echo "- Changing owner of '$1' to '$CELERYD_USER'"
+            chown "$CELERYD_USER" "$1"
+            maybe_die "Couldn't change owner of $1"
+        fi
+        if [ -n "$CELERYD_GROUP" ]; then
+            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
+            chgrp "$CELERYD_GROUP" "$1"
+            maybe_die "Couldn't change group of $1"
+        fi
+    fi
+}
+check_paths() {
+    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_LOG_DIR"
+    fi
+    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_PID_DIR"
+    fi
+}
+create_paths() {
+    create_default_dir "$CELERYD_LOG_DIR"
+    create_default_dir "$CELERYD_PID_DIR"
+}
+export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
+_get_pidfiles () {
+    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
+    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
+}
+_get_pids() {
+    found_pids=0
+    my_exitcode=0
+    for pidfile in $(_get_pidfiles); do
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+            my_exitcode=1
+        else
+            found_pids=1
+            echo "$pid"
+        fi
+    if [ $found_pids -eq 0 ]; then
+        echo "${SCRIPT_NAME}: All nodes down"
+        exit $my_exitcode
+    fi
+    done
+}
+_chuid () {
+    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
+}
+start_workers () {
+    if [ ! -z "$CELERYD_ULIMIT" ]; then
+        ulimit $CELERYD_ULIMIT
+    fi
+    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
+                 --pidfile="$CELERYD_PID_FILE"      \
+                 --logfile="$CELERYD_LOG_FILE"      \
+                 --loglevel="$CELERYD_LOG_LEVEL"    \
+                 $CELERY_APP_ARG                    \
+                 $CELERYD_OPTS
+}
+dryrun () {
+    (C_FAKEFORK=1 start_workers --verbose)
+}
+stop_workers () {
+    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers () {
+    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
+                   --pidfile="$CELERYD_PID_FILE"    \
+                   --logfile="$CELERYD_LOG_FILE"    \
+                   --loglevel="$CELERYD_LOG_LEVEL"  \
+                   $CELERY_APP_ARG                  \
+                   $CELERYD_OPTS
+}
+kill_workers() {
+    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers_graceful () {
+    echo "WARNING: Use with caution in production"
+    echo "The workers will attempt to restart, but they may not be able to."
+    local worker_pids=
+    worker_pids=`_get_pids`
+    [ "$one_failed" ] && exit 1
+    for worker_pid in $worker_pids; do
+        local failed=
+        kill -HUP $worker_pid 2> /dev/null || failed=true
+        if [ "$failed" ]; then
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
+            one_failed=true
+        else
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+check_status () {
+    my_exitcode=0
+    found_pids=0
+    local one_failed=
+    for pidfile in $(_get_pidfiles); do
+        if [ ! -r $pidfile ]; then
+            echo "${SCRIPT_NAME} down: no pidfiles found"
+            one_failed=true
+            break
+        fi
+        local node=`basename "$pidfile" .pid`
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+        else
+            local failed=
+            kill -0 $pid 2> /dev/null || failed=true
+            if [ "$failed" ]; then
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
+                one_failed=true
+            else
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
+            fi
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+case "$1" in
+    start)
+        check_dev_null
+        check_paths
+        start_workers
+    ;;
+    stop)
+        check_dev_null
+        check_paths
+        stop_workers
+    ;;
+    reload|force-reload)
+        echo "Use restart"
+    ;;
+    status)
+        check_status
+    ;;
+    restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    graceful)
+        check_dev_null
+        restart_workers_graceful
+    ;;
+    kill)
+        check_dev_null
+        kill_workers
+    ;;
+    dryrun)
+        check_dev_null
+        dryrun
+    ;;
+    try-restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    create-paths)
+        check_dev_null
+        create_paths
+    ;;
+    check-paths)
+        check_dev_null
+        check_paths
+    ;;
+    *)
+        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
+        exit 64  # EX_USAGE
+    ;;
+esac
+exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-staging b/python_apps/airtime-celery/install/initd/airtime-celery-staging
new file mode 100644
index 000000000..bed848a77
--- /dev/null
+++ b/python_apps/airtime-celery/install/initd/airtime-celery-staging
@@ -0,0 +1,334 @@
+#!/bin/sh -e
+# ============================================
+#  celeryd - Starts the Celery worker daemon.
+# ============================================
+#
+# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
+# :Configuration file: /etc/default/celeryd
+#
+# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
+
+
+### BEGIN INIT INFO
+# Provides:          celeryd
+# Required-Start:    $network $local_fs $remote_fs
+# Required-Stop:     $network $local_fs $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: celery task worker daemon
+### END INIT INFO
+#
+#
+# To implement separate init scripts, copy this script and give it a different
+# name:
+# I.e., if my new application, "little-worker" needs an init, I
+# should just use:
+#
+#   cp /etc/init.d/celeryd /etc/init.d/little-worker
+#
+# You can then configure this by manipulating /etc/default/little-worker.
+#
+VERSION=10.1
+echo "celery init v${VERSION}."
+if [ $(id -u) -ne 0 ]; then
+    echo "Error: This program can only be used by the root user."
+    echo "       Unprivileged users must use the 'celery multi' utility, "
+    echo "       or 'celery worker --detach'."
+    exit 1
+fi
+
+export RMQ_CONFIG_FILE="/etc/airtime-saas/staging/rabbitmq-analyzer.ini"
+
+# Can be a runlevel symlink (e.g. S02celeryd)
+if [ -L "$0" ]; then
+    SCRIPT_FILE=$(readlink "$0")
+else
+    SCRIPT_FILE="$0"
+fi
+SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
+
+DEFAULT_USER="celery"
+DEFAULT_PID_FILE="/var/run/celery/%n.pid"
+DEFAULT_LOG_FILE="/var/log/celery/%n.log"
+DEFAULT_LOG_LEVEL="INFO"
+DEFAULT_NODES="celery"
+DEFAULT_CELERYD="-m celery worker --detach"
+
+CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
+# Make sure executable configuration script is owned by root
+_config_sanity() {
+    local path="$1"
+    local owner=$(ls -ld "$path" | awk '{print $3}')
+    local iwgrp=$(ls -ld "$path" | cut -b 6)
+    local iwoth=$(ls -ld "$path" | cut -b 9)
+    if [ "$(id -u $owner)" != "0" ]; then
+        echo "Error: Config script '$path' must be owned by root!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with mailicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change ownership of the script:"
+        echo "    $ sudo chown root '$path'"
+        exit 1
+    fi
+    if [ "$iwoth" != "-" ]; then  # S_IWOTH
+        echo "Error: Config script '$path' cannot be writable by others!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
+        echo "Error: Config script '$path' cannot be writable by group!"
+        echo
+        echo "Resolution:"
+        echo "Review the file carefully and make sure it has not been "
+        echo "modified with malicious intent.  When sure the "
+        echo "script is safe to execute with superuser privileges "
+        echo "you can change the scripts permissions:"
+        echo "    $ sudo chmod 640 '$path'"
+        exit 1
+    fi
+}
+if [ -f "$CELERY_DEFAULTS" ]; then
+    _config_sanity "$CELERY_DEFAULTS"
+    echo "Using config script: $CELERY_DEFAULTS"
+    . "$CELERY_DEFAULTS"
+fi
+# Sets --app argument for CELERY_BIN
+CELERY_APP_ARG=""
+if [ ! -z "$CELERY_APP" ]; then
+    CELERY_APP_ARG="--app=$CELERY_APP"
+fi
+CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
+# Set CELERY_CREATE_DIRS to always create log/pid dirs.
+CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
+CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
+CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
+if [ -z "$CELERYD_PID_FILE" ]; then
+    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
+    CELERY_CREATE_RUNDIR=1
+fi
+if [ -z "$CELERYD_LOG_FILE" ]; then
+    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
+    CELERY_CREATE_LOGDIR=1
+fi
+CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
+CELERY_BIN=${CELERY_BIN:-"celery"}
+CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
+CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
+export CELERY_LOADER
+if [ -n "$2" ]; then
+    CELERYD_OPTS="$CELERYD_OPTS $2"
+fi
+CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
+CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
+# Extra start-stop-daemon options, like user/group.
+if [ -n "$CELERYD_CHDIR" ]; then
+    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
+fi
+check_dev_null() {
+    if [ ! -c /dev/null ]; then
+        echo "/dev/null is not a character device!"
+        exit 75  # EX_TEMPFAIL
+    fi
+}
+maybe_die() {
+    if [ $? -ne 0 ]; then
+        echo "Exiting: $* (errno $?)"
+        exit 77  # EX_NOPERM
+    fi
+}
+create_default_dir() {
+    if [ ! -d "$1" ]; then
+        echo "- Creating default directory: '$1'"
+        mkdir -p "$1"
+        maybe_die "Couldn't create directory $1"
+        echo "- Changing permissions of '$1' to 02755"
+        chmod 02755 "$1"
+        maybe_die "Couldn't change permissions for $1"
+        if [ -n "$CELERYD_USER" ]; then
+            echo "- Changing owner of '$1' to '$CELERYD_USER'"
+            chown "$CELERYD_USER" "$1"
+            maybe_die "Couldn't change owner of $1"
+        fi
+        if [ -n "$CELERYD_GROUP" ]; then
+            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
+            chgrp "$CELERYD_GROUP" "$1"
+            maybe_die "Couldn't change group of $1"
+        fi
+    fi
+}
+check_paths() {
+    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_LOG_DIR"
+    fi
+    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
+        create_default_dir "$CELERYD_PID_DIR"
+    fi
+}
+create_paths() {
+    create_default_dir "$CELERYD_LOG_DIR"
+    create_default_dir "$CELERYD_PID_DIR"
+}
+export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
+_get_pidfiles () {
+    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
+    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
+}
+_get_pids() {
+    found_pids=0
+    my_exitcode=0
+    for pidfile in $(_get_pidfiles); do
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+            my_exitcode=1
+        else
+            found_pids=1
+            echo "$pid"
+        fi
+    if [ $found_pids -eq 0 ]; then
+        echo "${SCRIPT_NAME}: All nodes down"
+        exit $my_exitcode
+    fi
+    done
+}
+_chuid () {
+    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
+}
+start_workers () {
+    if [ ! -z "$CELERYD_ULIMIT" ]; then
+        ulimit $CELERYD_ULIMIT
+    fi
+    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
+                 --pidfile="$CELERYD_PID_FILE"      \
+                 --logfile="$CELERYD_LOG_FILE"      \
+                 --loglevel="$CELERYD_LOG_LEVEL"    \
+                 $CELERY_APP_ARG                    \
+                 $CELERYD_OPTS
+}
+dryrun () {
+    (C_FAKEFORK=1 start_workers --verbose)
+}
+stop_workers () {
+    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers () {
+    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
+                   --pidfile="$CELERYD_PID_FILE"    \
+                   --logfile="$CELERYD_LOG_FILE"    \
+                   --loglevel="$CELERYD_LOG_LEVEL"  \
+                   $CELERY_APP_ARG                  \
+                   $CELERYD_OPTS
+}
+kill_workers() {
+    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
+}
+restart_workers_graceful () {
+    echo "WARNING: Use with caution in production"
+    echo "The workers will attempt to restart, but they may not be able to."
+    local worker_pids=
+    worker_pids=`_get_pids`
+    [ "$one_failed" ] && exit 1
+    for worker_pid in $worker_pids; do
+        local failed=
+        kill -HUP $worker_pid 2> /dev/null || failed=true
+        if [ "$failed" ]; then
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
+            one_failed=true
+        else
+            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+check_status () {
+    my_exitcode=0
+    found_pids=0
+    local one_failed=
+    for pidfile in $(_get_pidfiles); do
+        if [ ! -r $pidfile ]; then
+            echo "${SCRIPT_NAME} down: no pidfiles found"
+            one_failed=true
+            break
+        fi
+        local node=`basename "$pidfile" .pid`
+        local pid=`cat "$pidfile"`
+        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
+        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
+            echo "bad pid file ($pidfile)"
+            one_failed=true
+        else
+            local failed=
+            kill -0 $pid 2> /dev/null || failed=true
+            if [ "$failed" ]; then
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
+                one_failed=true
+            else
+                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
+            fi
+        fi
+    done
+    [ "$one_failed" ] && exit 1 || exit 0
+}
+case "$1" in
+    start)
+        check_dev_null
+        check_paths
+        start_workers
+    ;;
+    stop)
+        check_dev_null
+        check_paths
+        stop_workers
+    ;;
+    reload|force-reload)
+        echo "Use restart"
+    ;;
+    status)
+        check_status
+    ;;
+    restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    graceful)
+        check_dev_null
+        restart_workers_graceful
+    ;;
+    kill)
+        check_dev_null
+        kill_workers
+    ;;
+    dryrun)
+        check_dev_null
+        dryrun
+    ;;
+    try-restart)
+        check_dev_null
+        check_paths
+        restart_workers
+    ;;
+    create-paths)
+        check_dev_null
+        create_paths
+    ;;
+    check-paths)
+        check_dev_null
+        check_paths
+    ;;
+    *)
+        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
+        exit 64  # EX_USAGE
+    ;;
+esac
+exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 6f40351b7..96a5ef736 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -2,15 +2,26 @@ from setuptools import setup
 from subprocess import call
 import os
 import sys
+from glob import glob
 
 install_args = ['install', 'install_data', 'develop']
 
-# Definitely not the best way of doing this...
+# XXX Definitely not the best way of doing this... quite possibly the literal worst!
 if sys.argv[1] in install_args:
     data_files = [('/etc/default', ['install/conf/airtime-celery']),
-                  ('/etc/init.d', ['install/upstart/airtime-celery'])]
+                  ('/etc/init.d', ['install/initd/airtime-celery'])]
+    for i, arg in enumerate(sys.argv):
+        if "--dev-env" in arg:
+            env = arg.split('=')[1]
+            data_files = [('/etc/default', ['install/conf/airtime-celery-%s' % env]),
+                          ('/etc/init.d', ['install/initd/airtime-celery-%s' % env])]
+            sys.argv.remove(arg)
+        elif arg == "--all-envs":
+            data_files = ([('/etc/default', glob('install/conf/*')),
+                           ('/etc/init.d', glob('install/initd/*'))])
+            sys.argv.remove(arg)
 else:
-    data_files = []
+    scripts = data_files = []
 
 
 def postinst():
@@ -18,11 +29,13 @@ def postinst():
     call(['initctl', 'reload-configuration'])
     # Make /etc/init.d file executable and set proper
     # permissions for the defaults config file
-    os.chmod('/etc/init.d/airtime-celery', 0755)
-    os.chmod('/etc/default/airtime-celery', 0640)
-    print "Setting uploader to start on boot"
-    call(['update-rc.d', 'airtime-celery', 'defaults'])
-    print "Run \"sudo service airtime-celery restart\" now."
+    for f in glob('/etc/init.d/airtime-celery*'):
+        os.chmod(f, 0755)
+    for f in glob('/etc/default/airtime-celery*'):
+        os.chmod(f, 0640)
+    # print "Setting Celery to start on boot"
+    # call(['update-rc.d', 'airtime-celery', 'defaults'])
+    print "Run \"sudo service airtime-celery restart\" or \"sudo service airtime-celery-%DEV_ENV% restart\" now."
 
 setup(name='airtime-celery',
       version='0.1',
@@ -42,4 +55,3 @@ setup(name='airtime-celery',
 
 if data_files:
     postinst()
-

From 272911fa7b96d05c0c139abf0408678d9aac801a Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Fri, 12 Jun 2015 12:50:35 -0400
Subject: [PATCH 08/39] Remove saas-specific configurations

---
 python_apps/airtime-celery/README.rst         |   8 -
 .../install/conf/airtime-celery-bananas       |  25 --
 .../install/conf/airtime-celery-cliff         |  25 --
 .../install/conf/airtime-celery-production    |  25 --
 .../install/conf/airtime-celery-staging       |  25 --
 .../install/initd/airtime-celery-bananas      | 334 ------------------
 .../install/initd/airtime-celery-cliff        | 334 ------------------
 .../install/initd/airtime-celery-production   | 334 ------------------
 .../install/initd/airtime-celery-staging      | 334 ------------------
 python_apps/airtime-celery/setup.py           |  26 +-
 10 files changed, 7 insertions(+), 1463 deletions(-)
 delete mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-bananas
 delete mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-cliff
 delete mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-production
 delete mode 100644 python_apps/airtime-celery/install/conf/airtime-celery-staging
 delete mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-bananas
 delete mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-cliff
 delete mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-production
 delete mode 100644 python_apps/airtime-celery/install/initd/airtime-celery-staging

diff --git a/python_apps/airtime-celery/README.rst b/python_apps/airtime-celery/README.rst
index 7de00cf7b..f079f78f6 100644
--- a/python_apps/airtime-celery/README.rst
+++ b/python_apps/airtime-celery/README.rst
@@ -9,14 +9,6 @@ Installation
 
     $ sudo python setup.py install
 
-To install the configuration and upstart files for all environments (development and production)
-
-    $ sudo python setup.py install --all-envs
-
-You can also specify a single environment to deploy config and upstart files for:
-
-    $ sudo python setup.py install --dev-env=bananas
-
 Each instance of airtime-celery has its own worker, and multiple instances can be run in parallel.
 `Celery is thread-safe`_, so this parallelization won't cause conflicts.
 
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-bananas b/python_apps/airtime-celery/install/conf/airtime-celery-bananas
deleted file mode 100644
index 15cd5d8b3..000000000
--- a/python_apps/airtime-celery/install/conf/airtime-celery-bananas
+++ /dev/null
@@ -1,25 +0,0 @@
-# Names of nodes to start
-CELERYD_NODES="airtime-celery-bananas"
-
-# Absolute or relative path to the 'celery' command:
-CELERY_BIN="/usr/local/bin/celery"
-
-# App instance to use
-CELERY_APP="airtime-celery.tasks:celery"
-
-# Extra command-line arguments to the worker
-CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
-
-# %N will be replaced with the first part of the nodename.
-CELERYD_LOG_FILE="/var/log/airtime/%N.log"
-CELERYD_PID_FILE="/var/run/celery/%N.pid"
-
-# Workers should run as an unprivileged user.
-#   You need to create this user manually (or you can choose
-#   a user/group combination that already exists, e.g. nobody).
-CELERYD_USER="celery"
-CELERYD_GROUP="celery"
-
-# If enabled pid and log directories will be created if missing,
-# and owned by the userid/group configured.
-CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-cliff b/python_apps/airtime-celery/install/conf/airtime-celery-cliff
deleted file mode 100644
index a08e14bde..000000000
--- a/python_apps/airtime-celery/install/conf/airtime-celery-cliff
+++ /dev/null
@@ -1,25 +0,0 @@
-# Names of nodes to start
-CELERYD_NODES="airtime-celery-cliff"
-
-# Absolute or relative path to the 'celery' command:
-CELERY_BIN="/usr/local/bin/celery"
-
-# App instance to use
-CELERY_APP="airtime-celery.tasks:celery"
-
-# Extra command-line arguments to the worker
-CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
-
-# %N will be replaced with the first part of the nodename.
-CELERYD_LOG_FILE="/var/log/airtime/%N.log"
-CELERYD_PID_FILE="/var/run/celery/%N.pid"
-
-# Workers should run as an unprivileged user.
-#   You need to create this user manually (or you can choose
-#   a user/group combination that already exists, e.g. nobody).
-CELERYD_USER="celery"
-CELERYD_GROUP="celery"
-
-# If enabled pid and log directories will be created if missing,
-# and owned by the userid/group configured.
-CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-production b/python_apps/airtime-celery/install/conf/airtime-celery-production
deleted file mode 100644
index 69f8e6b7a..000000000
--- a/python_apps/airtime-celery/install/conf/airtime-celery-production
+++ /dev/null
@@ -1,25 +0,0 @@
-# Names of nodes to start
-CELERYD_NODES="airtime-celery-production"
-
-# Absolute or relative path to the 'celery' command:
-CELERY_BIN="/usr/local/bin/celery"
-
-# App instance to use
-CELERY_APP="airtime-celery.tasks:celery"
-
-# Extra command-line arguments to the worker
-CELERYD_OPTS="--time-limit=300 --concurrency=8 --config=celeryconfig"
-
-# %N will be replaced with the first part of the nodename.
-CELERYD_LOG_FILE="/var/log/airtime/%N.log"
-CELERYD_PID_FILE="/var/run/celery/%N.pid"
-
-# Workers should run as an unprivileged user.
-#   You need to create this user manually (or you can choose
-#   a user/group combination that already exists, e.g. nobody).
-CELERYD_USER="celery"
-CELERYD_GROUP="celery"
-
-# If enabled pid and log directories will be created if missing,
-# and owned by the userid/group configured.
-CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/conf/airtime-celery-staging b/python_apps/airtime-celery/install/conf/airtime-celery-staging
deleted file mode 100644
index e85252291..000000000
--- a/python_apps/airtime-celery/install/conf/airtime-celery-staging
+++ /dev/null
@@ -1,25 +0,0 @@
-# Names of nodes to start
-CELERYD_NODES="airtime-celery-staging"
-
-# Absolute or relative path to the 'celery' command:
-CELERY_BIN="/usr/local/bin/celery"
-
-# App instance to use
-CELERY_APP="airtime-celery.tasks:celery"
-
-# Extra command-line arguments to the worker
-CELERYD_OPTS="--time-limit=300 --concurrency=1 --config=celeryconfig"
-
-# %N will be replaced with the first part of the nodename.
-CELERYD_LOG_FILE="/var/log/airtime/%N.log"
-CELERYD_PID_FILE="/var/run/celery/%N.pid"
-
-# Workers should run as an unprivileged user.
-#   You need to create this user manually (or you can choose
-#   a user/group combination that already exists, e.g. nobody).
-CELERYD_USER="celery"
-CELERYD_GROUP="celery"
-
-# If enabled pid and log directories will be created if missing,
-# and owned by the userid/group configured.
-CELERY_CREATE_DIRS=1
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-bananas b/python_apps/airtime-celery/install/initd/airtime-celery-bananas
deleted file mode 100644
index c91c36713..000000000
--- a/python_apps/airtime-celery/install/initd/airtime-celery-bananas
+++ /dev/null
@@ -1,334 +0,0 @@
-#!/bin/sh -e
-# ============================================
-#  celeryd - Starts the Celery worker daemon.
-# ============================================
-#
-# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
-# :Configuration file: /etc/default/celeryd
-#
-# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
-
-
-### BEGIN INIT INFO
-# Provides:          celeryd
-# Required-Start:    $network $local_fs $remote_fs
-# Required-Stop:     $network $local_fs $remote_fs
-# Default-Start:     2 3 4 5
-# Default-Stop:      0 1 6
-# Short-Description: celery task worker daemon
-### END INIT INFO
-#
-#
-# To implement separate init scripts, copy this script and give it a different
-# name:
-# I.e., if my new application, "little-worker" needs an init, I
-# should just use:
-#
-#   cp /etc/init.d/celeryd /etc/init.d/little-worker
-#
-# You can then configure this by manipulating /etc/default/little-worker.
-#
-VERSION=10.1
-echo "celery init v${VERSION}."
-if [ $(id -u) -ne 0 ]; then
-    echo "Error: This program can only be used by the root user."
-    echo "       Unprivileged users must use the 'celery multi' utility, "
-    echo "       or 'celery worker --detach'."
-    exit 1
-fi
-
-export RMQ_CONFIG_FILE="/etc/airtime-saas/bananas/rabbitmq-analyzer.ini"
-
-# Can be a runlevel symlink (e.g. S02celeryd)
-if [ -L "$0" ]; then
-    SCRIPT_FILE=$(readlink "$0")
-else
-    SCRIPT_FILE="$0"
-fi
-SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
-
-DEFAULT_USER="celery"
-DEFAULT_PID_FILE="/var/run/celery/%n.pid"
-DEFAULT_LOG_FILE="/var/log/celery/%n.log"
-DEFAULT_LOG_LEVEL="INFO"
-DEFAULT_NODES="celery"
-DEFAULT_CELERYD="-m celery worker --detach"
-
-CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
-# Make sure executable configuration script is owned by root
-_config_sanity() {
-    local path="$1"
-    local owner=$(ls -ld "$path" | awk '{print $3}')
-    local iwgrp=$(ls -ld "$path" | cut -b 6)
-    local iwoth=$(ls -ld "$path" | cut -b 9)
-    if [ "$(id -u $owner)" != "0" ]; then
-        echo "Error: Config script '$path' must be owned by root!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with mailicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change ownership of the script:"
-        echo "    $ sudo chown root '$path'"
-        exit 1
-    fi
-    if [ "$iwoth" != "-" ]; then  # S_IWOTH
-        echo "Error: Config script '$path' cannot be writable by others!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
-        echo "Error: Config script '$path' cannot be writable by group!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-}
-if [ -f "$CELERY_DEFAULTS" ]; then
-    _config_sanity "$CELERY_DEFAULTS"
-    echo "Using config script: $CELERY_DEFAULTS"
-    . "$CELERY_DEFAULTS"
-fi
-# Sets --app argument for CELERY_BIN
-CELERY_APP_ARG=""
-if [ ! -z "$CELERY_APP" ]; then
-    CELERY_APP_ARG="--app=$CELERY_APP"
-fi
-CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
-# Set CELERY_CREATE_DIRS to always create log/pid dirs.
-CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
-CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
-CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
-if [ -z "$CELERYD_PID_FILE" ]; then
-    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
-    CELERY_CREATE_RUNDIR=1
-fi
-if [ -z "$CELERYD_LOG_FILE" ]; then
-    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
-    CELERY_CREATE_LOGDIR=1
-fi
-CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
-CELERY_BIN=${CELERY_BIN:-"celery"}
-CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
-CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
-export CELERY_LOADER
-if [ -n "$2" ]; then
-    CELERYD_OPTS="$CELERYD_OPTS $2"
-fi
-CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
-CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
-# Extra start-stop-daemon options, like user/group.
-if [ -n "$CELERYD_CHDIR" ]; then
-    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
-fi
-check_dev_null() {
-    if [ ! -c /dev/null ]; then
-        echo "/dev/null is not a character device!"
-        exit 75  # EX_TEMPFAIL
-    fi
-}
-maybe_die() {
-    if [ $? -ne 0 ]; then
-        echo "Exiting: $* (errno $?)"
-        exit 77  # EX_NOPERM
-    fi
-}
-create_default_dir() {
-    if [ ! -d "$1" ]; then
-        echo "- Creating default directory: '$1'"
-        mkdir -p "$1"
-        maybe_die "Couldn't create directory $1"
-        echo "- Changing permissions of '$1' to 02755"
-        chmod 02755 "$1"
-        maybe_die "Couldn't change permissions for $1"
-        if [ -n "$CELERYD_USER" ]; then
-            echo "- Changing owner of '$1' to '$CELERYD_USER'"
-            chown "$CELERYD_USER" "$1"
-            maybe_die "Couldn't change owner of $1"
-        fi
-        if [ -n "$CELERYD_GROUP" ]; then
-            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
-            chgrp "$CELERYD_GROUP" "$1"
-            maybe_die "Couldn't change group of $1"
-        fi
-    fi
-}
-check_paths() {
-    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_LOG_DIR"
-    fi
-    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_PID_DIR"
-    fi
-}
-create_paths() {
-    create_default_dir "$CELERYD_LOG_DIR"
-    create_default_dir "$CELERYD_PID_DIR"
-}
-export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
-_get_pidfiles () {
-    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
-    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
-}
-_get_pids() {
-    found_pids=0
-    my_exitcode=0
-    for pidfile in $(_get_pidfiles); do
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-            my_exitcode=1
-        else
-            found_pids=1
-            echo "$pid"
-        fi
-    if [ $found_pids -eq 0 ]; then
-        echo "${SCRIPT_NAME}: All nodes down"
-        exit $my_exitcode
-    fi
-    done
-}
-_chuid () {
-    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
-}
-start_workers () {
-    if [ ! -z "$CELERYD_ULIMIT" ]; then
-        ulimit $CELERYD_ULIMIT
-    fi
-    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
-                 --pidfile="$CELERYD_PID_FILE"      \
-                 --logfile="$CELERYD_LOG_FILE"      \
-                 --loglevel="$CELERYD_LOG_LEVEL"    \
-                 $CELERY_APP_ARG                    \
-                 $CELERYD_OPTS
-}
-dryrun () {
-    (C_FAKEFORK=1 start_workers --verbose)
-}
-stop_workers () {
-    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers () {
-    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
-                   --pidfile="$CELERYD_PID_FILE"    \
-                   --logfile="$CELERYD_LOG_FILE"    \
-                   --loglevel="$CELERYD_LOG_LEVEL"  \
-                   $CELERY_APP_ARG                  \
-                   $CELERYD_OPTS
-}
-kill_workers() {
-    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers_graceful () {
-    echo "WARNING: Use with caution in production"
-    echo "The workers will attempt to restart, but they may not be able to."
-    local worker_pids=
-    worker_pids=`_get_pids`
-    [ "$one_failed" ] && exit 1
-    for worker_pid in $worker_pids; do
-        local failed=
-        kill -HUP $worker_pid 2> /dev/null || failed=true
-        if [ "$failed" ]; then
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
-            one_failed=true
-        else
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-check_status () {
-    my_exitcode=0
-    found_pids=0
-    local one_failed=
-    for pidfile in $(_get_pidfiles); do
-        if [ ! -r $pidfile ]; then
-            echo "${SCRIPT_NAME} down: no pidfiles found"
-            one_failed=true
-            break
-        fi
-        local node=`basename "$pidfile" .pid`
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-        else
-            local failed=
-            kill -0 $pid 2> /dev/null || failed=true
-            if [ "$failed" ]; then
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
-                one_failed=true
-            else
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
-            fi
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-case "$1" in
-    start)
-        check_dev_null
-        check_paths
-        start_workers
-    ;;
-    stop)
-        check_dev_null
-        check_paths
-        stop_workers
-    ;;
-    reload|force-reload)
-        echo "Use restart"
-    ;;
-    status)
-        check_status
-    ;;
-    restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    graceful)
-        check_dev_null
-        restart_workers_graceful
-    ;;
-    kill)
-        check_dev_null
-        kill_workers
-    ;;
-    dryrun)
-        check_dev_null
-        dryrun
-    ;;
-    try-restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    create-paths)
-        check_dev_null
-        create_paths
-    ;;
-    check-paths)
-        check_dev_null
-        check_paths
-    ;;
-    *)
-        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
-        exit 64  # EX_USAGE
-    ;;
-esac
-exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-cliff b/python_apps/airtime-celery/install/initd/airtime-celery-cliff
deleted file mode 100644
index a2d0291a8..000000000
--- a/python_apps/airtime-celery/install/initd/airtime-celery-cliff
+++ /dev/null
@@ -1,334 +0,0 @@
-#!/bin/sh -e
-# ============================================
-#  celeryd - Starts the Celery worker daemon.
-# ============================================
-#
-# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
-# :Configuration file: /etc/default/celeryd
-#
-# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
-
-
-### BEGIN INIT INFO
-# Provides:          celeryd
-# Required-Start:    $network $local_fs $remote_fs
-# Required-Stop:     $network $local_fs $remote_fs
-# Default-Start:     2 3 4 5
-# Default-Stop:      0 1 6
-# Short-Description: celery task worker daemon
-### END INIT INFO
-#
-#
-# To implement separate init scripts, copy this script and give it a different
-# name:
-# I.e., if my new application, "little-worker" needs an init, I
-# should just use:
-#
-#   cp /etc/init.d/celeryd /etc/init.d/little-worker
-#
-# You can then configure this by manipulating /etc/default/little-worker.
-#
-VERSION=10.1
-echo "celery init v${VERSION}."
-if [ $(id -u) -ne 0 ]; then
-    echo "Error: This program can only be used by the root user."
-    echo "       Unprivileged users must use the 'celery multi' utility, "
-    echo "       or 'celery worker --detach'."
-    exit 1
-fi
-
-export RMQ_CONFIG_FILE="/etc/airtime-saas/cliff/rabbitmq-analyzer.ini"
-
-# Can be a runlevel symlink (e.g. S02celeryd)
-if [ -L "$0" ]; then
-    SCRIPT_FILE=$(readlink "$0")
-else
-    SCRIPT_FILE="$0"
-fi
-SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
-
-DEFAULT_USER="celery"
-DEFAULT_PID_FILE="/var/run/celery/%n.pid"
-DEFAULT_LOG_FILE="/var/log/celery/%n.log"
-DEFAULT_LOG_LEVEL="INFO"
-DEFAULT_NODES="celery"
-DEFAULT_CELERYD="-m celery worker --detach"
-
-CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
-# Make sure executable configuration script is owned by root
-_config_sanity() {
-    local path="$1"
-    local owner=$(ls -ld "$path" | awk '{print $3}')
-    local iwgrp=$(ls -ld "$path" | cut -b 6)
-    local iwoth=$(ls -ld "$path" | cut -b 9)
-    if [ "$(id -u $owner)" != "0" ]; then
-        echo "Error: Config script '$path' must be owned by root!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with mailicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change ownership of the script:"
-        echo "    $ sudo chown root '$path'"
-        exit 1
-    fi
-    if [ "$iwoth" != "-" ]; then  # S_IWOTH
-        echo "Error: Config script '$path' cannot be writable by others!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
-        echo "Error: Config script '$path' cannot be writable by group!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-}
-if [ -f "$CELERY_DEFAULTS" ]; then
-    _config_sanity "$CELERY_DEFAULTS"
-    echo "Using config script: $CELERY_DEFAULTS"
-    . "$CELERY_DEFAULTS"
-fi
-# Sets --app argument for CELERY_BIN
-CELERY_APP_ARG=""
-if [ ! -z "$CELERY_APP" ]; then
-    CELERY_APP_ARG="--app=$CELERY_APP"
-fi
-CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
-# Set CELERY_CREATE_DIRS to always create log/pid dirs.
-CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
-CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
-CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
-if [ -z "$CELERYD_PID_FILE" ]; then
-    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
-    CELERY_CREATE_RUNDIR=1
-fi
-if [ -z "$CELERYD_LOG_FILE" ]; then
-    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
-    CELERY_CREATE_LOGDIR=1
-fi
-CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
-CELERY_BIN=${CELERY_BIN:-"celery"}
-CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
-CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
-export CELERY_LOADER
-if [ -n "$2" ]; then
-    CELERYD_OPTS="$CELERYD_OPTS $2"
-fi
-CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
-CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
-# Extra start-stop-daemon options, like user/group.
-if [ -n "$CELERYD_CHDIR" ]; then
-    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
-fi
-check_dev_null() {
-    if [ ! -c /dev/null ]; then
-        echo "/dev/null is not a character device!"
-        exit 75  # EX_TEMPFAIL
-    fi
-}
-maybe_die() {
-    if [ $? -ne 0 ]; then
-        echo "Exiting: $* (errno $?)"
-        exit 77  # EX_NOPERM
-    fi
-}
-create_default_dir() {
-    if [ ! -d "$1" ]; then
-        echo "- Creating default directory: '$1'"
-        mkdir -p "$1"
-        maybe_die "Couldn't create directory $1"
-        echo "- Changing permissions of '$1' to 02755"
-        chmod 02755 "$1"
-        maybe_die "Couldn't change permissions for $1"
-        if [ -n "$CELERYD_USER" ]; then
-            echo "- Changing owner of '$1' to '$CELERYD_USER'"
-            chown "$CELERYD_USER" "$1"
-            maybe_die "Couldn't change owner of $1"
-        fi
-        if [ -n "$CELERYD_GROUP" ]; then
-            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
-            chgrp "$CELERYD_GROUP" "$1"
-            maybe_die "Couldn't change group of $1"
-        fi
-    fi
-}
-check_paths() {
-    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_LOG_DIR"
-    fi
-    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_PID_DIR"
-    fi
-}
-create_paths() {
-    create_default_dir "$CELERYD_LOG_DIR"
-    create_default_dir "$CELERYD_PID_DIR"
-}
-export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
-_get_pidfiles () {
-    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
-    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
-}
-_get_pids() {
-    found_pids=0
-    my_exitcode=0
-    for pidfile in $(_get_pidfiles); do
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-            my_exitcode=1
-        else
-            found_pids=1
-            echo "$pid"
-        fi
-    if [ $found_pids -eq 0 ]; then
-        echo "${SCRIPT_NAME}: All nodes down"
-        exit $my_exitcode
-    fi
-    done
-}
-_chuid () {
-    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
-}
-start_workers () {
-    if [ ! -z "$CELERYD_ULIMIT" ]; then
-        ulimit $CELERYD_ULIMIT
-    fi
-    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
-                 --pidfile="$CELERYD_PID_FILE"      \
-                 --logfile="$CELERYD_LOG_FILE"      \
-                 --loglevel="$CELERYD_LOG_LEVEL"    \
-                 $CELERY_APP_ARG                    \
-                 $CELERYD_OPTS
-}
-dryrun () {
-    (C_FAKEFORK=1 start_workers --verbose)
-}
-stop_workers () {
-    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers () {
-    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
-                   --pidfile="$CELERYD_PID_FILE"    \
-                   --logfile="$CELERYD_LOG_FILE"    \
-                   --loglevel="$CELERYD_LOG_LEVEL"  \
-                   $CELERY_APP_ARG                  \
-                   $CELERYD_OPTS
-}
-kill_workers() {
-    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers_graceful () {
-    echo "WARNING: Use with caution in production"
-    echo "The workers will attempt to restart, but they may not be able to."
-    local worker_pids=
-    worker_pids=`_get_pids`
-    [ "$one_failed" ] && exit 1
-    for worker_pid in $worker_pids; do
-        local failed=
-        kill -HUP $worker_pid 2> /dev/null || failed=true
-        if [ "$failed" ]; then
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
-            one_failed=true
-        else
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-check_status () {
-    my_exitcode=0
-    found_pids=0
-    local one_failed=
-    for pidfile in $(_get_pidfiles); do
-        if [ ! -r $pidfile ]; then
-            echo "${SCRIPT_NAME} down: no pidfiles found"
-            one_failed=true
-            break
-        fi
-        local node=`basename "$pidfile" .pid`
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-        else
-            local failed=
-            kill -0 $pid 2> /dev/null || failed=true
-            if [ "$failed" ]; then
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
-                one_failed=true
-            else
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
-            fi
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-case "$1" in
-    start)
-        check_dev_null
-        check_paths
-        start_workers
-    ;;
-    stop)
-        check_dev_null
-        check_paths
-        stop_workers
-    ;;
-    reload|force-reload)
-        echo "Use restart"
-    ;;
-    status)
-        check_status
-    ;;
-    restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    graceful)
-        check_dev_null
-        restart_workers_graceful
-    ;;
-    kill)
-        check_dev_null
-        kill_workers
-    ;;
-    dryrun)
-        check_dev_null
-        dryrun
-    ;;
-    try-restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    create-paths)
-        check_dev_null
-        create_paths
-    ;;
-    check-paths)
-        check_dev_null
-        check_paths
-    ;;
-    *)
-        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
-        exit 64  # EX_USAGE
-    ;;
-esac
-exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-production b/python_apps/airtime-celery/install/initd/airtime-celery-production
deleted file mode 100644
index 43f98d2d5..000000000
--- a/python_apps/airtime-celery/install/initd/airtime-celery-production
+++ /dev/null
@@ -1,334 +0,0 @@
-#!/bin/sh -e
-# ============================================
-#  celeryd - Starts the Celery worker daemon.
-# ============================================
-#
-# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
-# :Configuration file: /etc/default/celeryd
-#
-# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
-
-
-### BEGIN INIT INFO
-# Provides:          celeryd
-# Required-Start:    $network $local_fs $remote_fs
-# Required-Stop:     $network $local_fs $remote_fs
-# Default-Start:     2 3 4 5
-# Default-Stop:      0 1 6
-# Short-Description: celery task worker daemon
-### END INIT INFO
-#
-#
-# To implement separate init scripts, copy this script and give it a different
-# name:
-# I.e., if my new application, "little-worker" needs an init, I
-# should just use:
-#
-#   cp /etc/init.d/celeryd /etc/init.d/little-worker
-#
-# You can then configure this by manipulating /etc/default/little-worker.
-#
-VERSION=10.1
-echo "celery init v${VERSION}."
-if [ $(id -u) -ne 0 ]; then
-    echo "Error: This program can only be used by the root user."
-    echo "       Unprivileged users must use the 'celery multi' utility, "
-    echo "       or 'celery worker --detach'."
-    exit 1
-fi
-
-export RMQ_CONFIG_FILE="/etc/airtime-saas/production/rabbitmq-analyzer.ini"
-
-# Can be a runlevel symlink (e.g. S02celeryd)
-if [ -L "$0" ]; then
-    SCRIPT_FILE=$(readlink "$0")
-else
-    SCRIPT_FILE="$0"
-fi
-SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
-
-DEFAULT_USER="celery"
-DEFAULT_PID_FILE="/var/run/celery/%n.pid"
-DEFAULT_LOG_FILE="/var/log/celery/%n.log"
-DEFAULT_LOG_LEVEL="INFO"
-DEFAULT_NODES="celery"
-DEFAULT_CELERYD="-m celery worker --detach"
-
-CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
-# Make sure executable configuration script is owned by root
-_config_sanity() {
-    local path="$1"
-    local owner=$(ls -ld "$path" | awk '{print $3}')
-    local iwgrp=$(ls -ld "$path" | cut -b 6)
-    local iwoth=$(ls -ld "$path" | cut -b 9)
-    if [ "$(id -u $owner)" != "0" ]; then
-        echo "Error: Config script '$path' must be owned by root!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with mailicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change ownership of the script:"
-        echo "    $ sudo chown root '$path'"
-        exit 1
-    fi
-    if [ "$iwoth" != "-" ]; then  # S_IWOTH
-        echo "Error: Config script '$path' cannot be writable by others!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
-        echo "Error: Config script '$path' cannot be writable by group!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-}
-if [ -f "$CELERY_DEFAULTS" ]; then
-    _config_sanity "$CELERY_DEFAULTS"
-    echo "Using config script: $CELERY_DEFAULTS"
-    . "$CELERY_DEFAULTS"
-fi
-# Sets --app argument for CELERY_BIN
-CELERY_APP_ARG=""
-if [ ! -z "$CELERY_APP" ]; then
-    CELERY_APP_ARG="--app=$CELERY_APP"
-fi
-CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
-# Set CELERY_CREATE_DIRS to always create log/pid dirs.
-CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
-CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
-CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
-if [ -z "$CELERYD_PID_FILE" ]; then
-    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
-    CELERY_CREATE_RUNDIR=1
-fi
-if [ -z "$CELERYD_LOG_FILE" ]; then
-    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
-    CELERY_CREATE_LOGDIR=1
-fi
-CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
-CELERY_BIN=${CELERY_BIN:-"celery"}
-CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
-CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
-export CELERY_LOADER
-if [ -n "$2" ]; then
-    CELERYD_OPTS="$CELERYD_OPTS $2"
-fi
-CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
-CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
-# Extra start-stop-daemon options, like user/group.
-if [ -n "$CELERYD_CHDIR" ]; then
-    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
-fi
-check_dev_null() {
-    if [ ! -c /dev/null ]; then
-        echo "/dev/null is not a character device!"
-        exit 75  # EX_TEMPFAIL
-    fi
-}
-maybe_die() {
-    if [ $? -ne 0 ]; then
-        echo "Exiting: $* (errno $?)"
-        exit 77  # EX_NOPERM
-    fi
-}
-create_default_dir() {
-    if [ ! -d "$1" ]; then
-        echo "- Creating default directory: '$1'"
-        mkdir -p "$1"
-        maybe_die "Couldn't create directory $1"
-        echo "- Changing permissions of '$1' to 02755"
-        chmod 02755 "$1"
-        maybe_die "Couldn't change permissions for $1"
-        if [ -n "$CELERYD_USER" ]; then
-            echo "- Changing owner of '$1' to '$CELERYD_USER'"
-            chown "$CELERYD_USER" "$1"
-            maybe_die "Couldn't change owner of $1"
-        fi
-        if [ -n "$CELERYD_GROUP" ]; then
-            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
-            chgrp "$CELERYD_GROUP" "$1"
-            maybe_die "Couldn't change group of $1"
-        fi
-    fi
-}
-check_paths() {
-    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_LOG_DIR"
-    fi
-    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_PID_DIR"
-    fi
-}
-create_paths() {
-    create_default_dir "$CELERYD_LOG_DIR"
-    create_default_dir "$CELERYD_PID_DIR"
-}
-export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
-_get_pidfiles () {
-    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
-    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
-}
-_get_pids() {
-    found_pids=0
-    my_exitcode=0
-    for pidfile in $(_get_pidfiles); do
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-            my_exitcode=1
-        else
-            found_pids=1
-            echo "$pid"
-        fi
-    if [ $found_pids -eq 0 ]; then
-        echo "${SCRIPT_NAME}: All nodes down"
-        exit $my_exitcode
-    fi
-    done
-}
-_chuid () {
-    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
-}
-start_workers () {
-    if [ ! -z "$CELERYD_ULIMIT" ]; then
-        ulimit $CELERYD_ULIMIT
-    fi
-    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
-                 --pidfile="$CELERYD_PID_FILE"      \
-                 --logfile="$CELERYD_LOG_FILE"      \
-                 --loglevel="$CELERYD_LOG_LEVEL"    \
-                 $CELERY_APP_ARG                    \
-                 $CELERYD_OPTS
-}
-dryrun () {
-    (C_FAKEFORK=1 start_workers --verbose)
-}
-stop_workers () {
-    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers () {
-    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
-                   --pidfile="$CELERYD_PID_FILE"    \
-                   --logfile="$CELERYD_LOG_FILE"    \
-                   --loglevel="$CELERYD_LOG_LEVEL"  \
-                   $CELERY_APP_ARG                  \
-                   $CELERYD_OPTS
-}
-kill_workers() {
-    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers_graceful () {
-    echo "WARNING: Use with caution in production"
-    echo "The workers will attempt to restart, but they may not be able to."
-    local worker_pids=
-    worker_pids=`_get_pids`
-    [ "$one_failed" ] && exit 1
-    for worker_pid in $worker_pids; do
-        local failed=
-        kill -HUP $worker_pid 2> /dev/null || failed=true
-        if [ "$failed" ]; then
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
-            one_failed=true
-        else
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-check_status () {
-    my_exitcode=0
-    found_pids=0
-    local one_failed=
-    for pidfile in $(_get_pidfiles); do
-        if [ ! -r $pidfile ]; then
-            echo "${SCRIPT_NAME} down: no pidfiles found"
-            one_failed=true
-            break
-        fi
-        local node=`basename "$pidfile" .pid`
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-        else
-            local failed=
-            kill -0 $pid 2> /dev/null || failed=true
-            if [ "$failed" ]; then
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
-                one_failed=true
-            else
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
-            fi
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-case "$1" in
-    start)
-        check_dev_null
-        check_paths
-        start_workers
-    ;;
-    stop)
-        check_dev_null
-        check_paths
-        stop_workers
-    ;;
-    reload|force-reload)
-        echo "Use restart"
-    ;;
-    status)
-        check_status
-    ;;
-    restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    graceful)
-        check_dev_null
-        restart_workers_graceful
-    ;;
-    kill)
-        check_dev_null
-        kill_workers
-    ;;
-    dryrun)
-        check_dev_null
-        dryrun
-    ;;
-    try-restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    create-paths)
-        check_dev_null
-        create_paths
-    ;;
-    check-paths)
-        check_dev_null
-        check_paths
-    ;;
-    *)
-        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
-        exit 64  # EX_USAGE
-    ;;
-esac
-exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/install/initd/airtime-celery-staging b/python_apps/airtime-celery/install/initd/airtime-celery-staging
deleted file mode 100644
index bed848a77..000000000
--- a/python_apps/airtime-celery/install/initd/airtime-celery-staging
+++ /dev/null
@@ -1,334 +0,0 @@
-#!/bin/sh -e
-# ============================================
-#  celeryd - Starts the Celery worker daemon.
-# ============================================
-#
-# :Usage: /etc/init.d/celeryd {start|stop|force-reload|restart|try-restart|status}
-# :Configuration file: /etc/default/celeryd
-#
-# See http://docs.celeryproject.org/en/latest/tutorials/daemonizing.html#generic-init-scripts
-
-
-### BEGIN INIT INFO
-# Provides:          celeryd
-# Required-Start:    $network $local_fs $remote_fs
-# Required-Stop:     $network $local_fs $remote_fs
-# Default-Start:     2 3 4 5
-# Default-Stop:      0 1 6
-# Short-Description: celery task worker daemon
-### END INIT INFO
-#
-#
-# To implement separate init scripts, copy this script and give it a different
-# name:
-# I.e., if my new application, "little-worker" needs an init, I
-# should just use:
-#
-#   cp /etc/init.d/celeryd /etc/init.d/little-worker
-#
-# You can then configure this by manipulating /etc/default/little-worker.
-#
-VERSION=10.1
-echo "celery init v${VERSION}."
-if [ $(id -u) -ne 0 ]; then
-    echo "Error: This program can only be used by the root user."
-    echo "       Unprivileged users must use the 'celery multi' utility, "
-    echo "       or 'celery worker --detach'."
-    exit 1
-fi
-
-export RMQ_CONFIG_FILE="/etc/airtime-saas/staging/rabbitmq-analyzer.ini"
-
-# Can be a runlevel symlink (e.g. S02celeryd)
-if [ -L "$0" ]; then
-    SCRIPT_FILE=$(readlink "$0")
-else
-    SCRIPT_FILE="$0"
-fi
-SCRIPT_NAME="$(basename "$SCRIPT_FILE")"
-
-DEFAULT_USER="celery"
-DEFAULT_PID_FILE="/var/run/celery/%n.pid"
-DEFAULT_LOG_FILE="/var/log/celery/%n.log"
-DEFAULT_LOG_LEVEL="INFO"
-DEFAULT_NODES="celery"
-DEFAULT_CELERYD="-m celery worker --detach"
-
-CELERY_DEFAULTS=${CELERY_DEFAULTS:-"/etc/default/${SCRIPT_NAME}"}
-# Make sure executable configuration script is owned by root
-_config_sanity() {
-    local path="$1"
-    local owner=$(ls -ld "$path" | awk '{print $3}')
-    local iwgrp=$(ls -ld "$path" | cut -b 6)
-    local iwoth=$(ls -ld "$path" | cut -b 9)
-    if [ "$(id -u $owner)" != "0" ]; then
-        echo "Error: Config script '$path' must be owned by root!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with mailicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change ownership of the script:"
-        echo "    $ sudo chown root '$path'"
-        exit 1
-    fi
-    if [ "$iwoth" != "-" ]; then  # S_IWOTH
-        echo "Error: Config script '$path' cannot be writable by others!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-    if [ "$iwgrp" != "-" ]; then  # S_IWGRP
-        echo "Error: Config script '$path' cannot be writable by group!"
-        echo
-        echo "Resolution:"
-        echo "Review the file carefully and make sure it has not been "
-        echo "modified with malicious intent.  When sure the "
-        echo "script is safe to execute with superuser privileges "
-        echo "you can change the scripts permissions:"
-        echo "    $ sudo chmod 640 '$path'"
-        exit 1
-    fi
-}
-if [ -f "$CELERY_DEFAULTS" ]; then
-    _config_sanity "$CELERY_DEFAULTS"
-    echo "Using config script: $CELERY_DEFAULTS"
-    . "$CELERY_DEFAULTS"
-fi
-# Sets --app argument for CELERY_BIN
-CELERY_APP_ARG=""
-if [ ! -z "$CELERY_APP" ]; then
-    CELERY_APP_ARG="--app=$CELERY_APP"
-fi
-CELERYD_USER=${CELERYD_USER:-$DEFAULT_USER}
-# Set CELERY_CREATE_DIRS to always create log/pid dirs.
-CELERY_CREATE_DIRS=${CELERY_CREATE_DIRS:-0}
-CELERY_CREATE_RUNDIR=$CELERY_CREATE_DIRS
-CELERY_CREATE_LOGDIR=$CELERY_CREATE_DIRS
-if [ -z "$CELERYD_PID_FILE" ]; then
-    CELERYD_PID_FILE="$DEFAULT_PID_FILE"
-    CELERY_CREATE_RUNDIR=1
-fi
-if [ -z "$CELERYD_LOG_FILE" ]; then
-    CELERYD_LOG_FILE="$DEFAULT_LOG_FILE"
-    CELERY_CREATE_LOGDIR=1
-fi
-CELERYD_LOG_LEVEL=${CELERYD_LOG_LEVEL:-${CELERYD_LOGLEVEL:-$DEFAULT_LOG_LEVEL}}
-CELERY_BIN=${CELERY_BIN:-"celery"}
-CELERYD_MULTI=${CELERYD_MULTI:-"$CELERY_BIN multi"}
-CELERYD_NODES=${CELERYD_NODES:-$DEFAULT_NODES}
-export CELERY_LOADER
-if [ -n "$2" ]; then
-    CELERYD_OPTS="$CELERYD_OPTS $2"
-fi
-CELERYD_LOG_DIR=`dirname $CELERYD_LOG_FILE`
-CELERYD_PID_DIR=`dirname $CELERYD_PID_FILE`
-# Extra start-stop-daemon options, like user/group.
-if [ -n "$CELERYD_CHDIR" ]; then
-    DAEMON_OPTS="$DAEMON_OPTS --workdir=$CELERYD_CHDIR"
-fi
-check_dev_null() {
-    if [ ! -c /dev/null ]; then
-        echo "/dev/null is not a character device!"
-        exit 75  # EX_TEMPFAIL
-    fi
-}
-maybe_die() {
-    if [ $? -ne 0 ]; then
-        echo "Exiting: $* (errno $?)"
-        exit 77  # EX_NOPERM
-    fi
-}
-create_default_dir() {
-    if [ ! -d "$1" ]; then
-        echo "- Creating default directory: '$1'"
-        mkdir -p "$1"
-        maybe_die "Couldn't create directory $1"
-        echo "- Changing permissions of '$1' to 02755"
-        chmod 02755 "$1"
-        maybe_die "Couldn't change permissions for $1"
-        if [ -n "$CELERYD_USER" ]; then
-            echo "- Changing owner of '$1' to '$CELERYD_USER'"
-            chown "$CELERYD_USER" "$1"
-            maybe_die "Couldn't change owner of $1"
-        fi
-        if [ -n "$CELERYD_GROUP" ]; then
-            echo "- Changing group of '$1' to '$CELERYD_GROUP'"
-            chgrp "$CELERYD_GROUP" "$1"
-            maybe_die "Couldn't change group of $1"
-        fi
-    fi
-}
-check_paths() {
-    if [ $CELERY_CREATE_LOGDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_LOG_DIR"
-    fi
-    if [ $CELERY_CREATE_RUNDIR -eq 1 ]; then
-        create_default_dir "$CELERYD_PID_DIR"
-    fi
-}
-create_paths() {
-    create_default_dir "$CELERYD_LOG_DIR"
-    create_default_dir "$CELERYD_PID_DIR"
-}
-export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
-_get_pidfiles () {
-    # note: multi < 3.1.14 output to stderr, not stdout, hence the redirect.
-    ${CELERYD_MULTI} expand "${CELERYD_PID_FILE}" ${CELERYD_NODES} 2>&1
-}
-_get_pids() {
-    found_pids=0
-    my_exitcode=0
-    for pidfile in $(_get_pidfiles); do
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-            my_exitcode=1
-        else
-            found_pids=1
-            echo "$pid"
-        fi
-    if [ $found_pids -eq 0 ]; then
-        echo "${SCRIPT_NAME}: All nodes down"
-        exit $my_exitcode
-    fi
-    done
-}
-_chuid () {
-    su "$CELERYD_USER" -c "$CELERYD_MULTI $*"
-}
-start_workers () {
-    if [ ! -z "$CELERYD_ULIMIT" ]; then
-        ulimit $CELERYD_ULIMIT
-    fi
-    _chuid $* start $CELERYD_NODES $DAEMON_OPTS     \
-                 --pidfile="$CELERYD_PID_FILE"      \
-                 --logfile="$CELERYD_LOG_FILE"      \
-                 --loglevel="$CELERYD_LOG_LEVEL"    \
-                 $CELERY_APP_ARG                    \
-                 $CELERYD_OPTS
-}
-dryrun () {
-    (C_FAKEFORK=1 start_workers --verbose)
-}
-stop_workers () {
-    _chuid stopwait $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers () {
-    _chuid restart $CELERYD_NODES $DAEMON_OPTS      \
-                   --pidfile="$CELERYD_PID_FILE"    \
-                   --logfile="$CELERYD_LOG_FILE"    \
-                   --loglevel="$CELERYD_LOG_LEVEL"  \
-                   $CELERY_APP_ARG                  \
-                   $CELERYD_OPTS
-}
-kill_workers() {
-    _chuid kill $CELERYD_NODES --pidfile="$CELERYD_PID_FILE"
-}
-restart_workers_graceful () {
-    echo "WARNING: Use with caution in production"
-    echo "The workers will attempt to restart, but they may not be able to."
-    local worker_pids=
-    worker_pids=`_get_pids`
-    [ "$one_failed" ] && exit 1
-    for worker_pid in $worker_pids; do
-        local failed=
-        kill -HUP $worker_pid 2> /dev/null || failed=true
-        if [ "$failed" ]; then
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) could not be restarted"
-            one_failed=true
-        else
-            echo "${SCRIPT_NAME} worker (pid $worker_pid) received SIGHUP"
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-check_status () {
-    my_exitcode=0
-    found_pids=0
-    local one_failed=
-    for pidfile in $(_get_pidfiles); do
-        if [ ! -r $pidfile ]; then
-            echo "${SCRIPT_NAME} down: no pidfiles found"
-            one_failed=true
-            break
-        fi
-        local node=`basename "$pidfile" .pid`
-        local pid=`cat "$pidfile"`
-        local cleaned_pid=`echo "$pid" | sed -e 's/[^0-9]//g'`
-        if [ -z "$pid" ] || [ "$cleaned_pid" != "$pid" ]; then
-            echo "bad pid file ($pidfile)"
-            one_failed=true
-        else
-            local failed=
-            kill -0 $pid 2> /dev/null || failed=true
-            if [ "$failed" ]; then
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is down, but pidfile exists!"
-                one_failed=true
-            else
-                echo "${SCRIPT_NAME} (node $node) (pid $pid) is up..."
-            fi
-        fi
-    done
-    [ "$one_failed" ] && exit 1 || exit 0
-}
-case "$1" in
-    start)
-        check_dev_null
-        check_paths
-        start_workers
-    ;;
-    stop)
-        check_dev_null
-        check_paths
-        stop_workers
-    ;;
-    reload|force-reload)
-        echo "Use restart"
-    ;;
-    status)
-        check_status
-    ;;
-    restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    graceful)
-        check_dev_null
-        restart_workers_graceful
-    ;;
-    kill)
-        check_dev_null
-        kill_workers
-    ;;
-    dryrun)
-        check_dev_null
-        dryrun
-    ;;
-    try-restart)
-        check_dev_null
-        check_paths
-        restart_workers
-    ;;
-    create-paths)
-        check_dev_null
-        create_paths
-    ;;
-    check-paths)
-        check_dev_null
-        check_paths
-    ;;
-    *)
-        echo "Usage: /etc/init.d/${SCRIPT_NAME} {start|stop|restart|graceful|kill|dryrun|create-paths}"
-        exit 64  # EX_USAGE
-    ;;
-esac
-exit 0
\ No newline at end of file
diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 96a5ef736..4604a2b31 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -6,22 +6,12 @@ from glob import glob
 
 install_args = ['install', 'install_data', 'develop']
 
-# XXX Definitely not the best way of doing this... quite possibly the literal worst!
-if sys.argv[1] in install_args:
+# XXX Definitely not the best way of doing this...
+if sys.argv[1] in install_args and "--no-init-script" not in sys.argv:
     data_files = [('/etc/default', ['install/conf/airtime-celery']),
                   ('/etc/init.d', ['install/initd/airtime-celery'])]
-    for i, arg in enumerate(sys.argv):
-        if "--dev-env" in arg:
-            env = arg.split('=')[1]
-            data_files = [('/etc/default', ['install/conf/airtime-celery-%s' % env]),
-                          ('/etc/init.d', ['install/initd/airtime-celery-%s' % env])]
-            sys.argv.remove(arg)
-        elif arg == "--all-envs":
-            data_files = ([('/etc/default', glob('install/conf/*')),
-                           ('/etc/init.d', glob('install/initd/*'))])
-            sys.argv.remove(arg)
 else:
-    scripts = data_files = []
+    data_files = []
 
 
 def postinst():
@@ -29,13 +19,11 @@ def postinst():
     call(['initctl', 'reload-configuration'])
     # Make /etc/init.d file executable and set proper
     # permissions for the defaults config file
-    for f in glob('/etc/init.d/airtime-celery*'):
-        os.chmod(f, 0755)
-    for f in glob('/etc/default/airtime-celery*'):
-        os.chmod(f, 0640)
+    os.chmod('/etc/init.d/airtime-celery', 0755)
+    os.chmod('/etc/default/airtime-celery', 0640)
     # print "Setting Celery to start on boot"
-    # call(['update-rc.d', 'airtime-celery', 'defaults'])
-    print "Run \"sudo service airtime-celery restart\" or \"sudo service airtime-celery-%DEV_ENV% restart\" now."
+    call(['update-rc.d', 'airtime-celery', 'defaults'])
+    print "Run \"sudo service airtime-celery restart\" now."
 
 setup(name='airtime-celery',
       version='0.1',

From 3902c8c746cb52378757612b46ac22b04ecf5bb3 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 15 Jun 2015 15:12:37 -0400
Subject: [PATCH 09/39] Add SoundCloud delete functionality and fixes;
 implement TaskManager to run background jobs

---
 airtime_mvc/application/Bootstrap.php         |   8 +-
 .../application/common/TaskManager.php        | 160 ++++++++++++++++++
 airtime_mvc/application/configs/constants.php |  17 +-
 .../controllers/ErrorController.php           |   1 -
 .../controllers/LibraryController.php         |   8 +-
 .../controllers/SoundcloudController.php      |   7 +-
 .../controllers/ThirdPartyController.php      |  30 ++--
 .../controllers/UpgradeController.php         |   3 +-
 airtime_mvc/application/models/RabbitMq.php   |  17 +-
 .../services/SoundcloudService.php            |  48 ++++--
 .../services/ThirdPartyService.php            | 128 +++++++++-----
 airtime_mvc/application/upgrade/Upgrades.php  |  22 ++-
 .../public/js/airtime/common/common.js        |   6 -
 .../airtime-celery/celeryconfig.py            |  22 +--
 .../airtime-celery/airtime-celery/tasks.py    |  23 ++-
 15 files changed, 373 insertions(+), 127 deletions(-)
 create mode 100644 airtime_mvc/application/common/TaskManager.php

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 6f71ff8a9..7f0bc7d7d 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -27,6 +27,7 @@ require_once "ProvisioningHelper.php";
 require_once "GoogleAnalytics.php";
 require_once "Timezone.php";
 require_once "Auth.php";
+require_once "TaskManager.php";
 require_once __DIR__.'/services/SoundcloudService.php';
 require_once __DIR__.'/forms/helpers/ValidationTypes.php';
 require_once __DIR__.'/forms/helpers/CustomDecorators.php';
@@ -123,15 +124,12 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
         $view->headScript()->appendScript("var COMPANY_NAME = '" . COMPANY_NAME . "';");
     }
     
-    protected function _initUpgrade() {
+    protected function _initTasks() {
         /* We need to wrap this here so that we aren't checking when we're running the unit test suite
          */
         if (getenv("AIRTIME_UNIT_TEST") != 1) {
             //This will do the upgrade too if it's needed...
-            if (UpgradeManager::checkIfUpgradeIsNeeded()) {
-                $upgradeManager = new UpgradeManager();
-                $upgradeManager->doUpgrade();
-            }
+            TaskManager::getInstance()->runTasks();
         }
     }
 
diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php
new file mode 100644
index 000000000..a52bfa063
--- /dev/null
+++ b/airtime_mvc/application/common/TaskManager.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * Class TaskManager
+ */
+final class TaskManager {
+
+    /**
+     * @var array tasks to be run
+     */
+    protected $_taskList = [
+        AirtimeTask::SOUNDCLOUD,
+        AirtimeTask::UPGRADE
+    ];
+
+    /**
+     * @var TaskManager singleton instance object
+     */
+    protected static $_instance;
+
+    /**
+     * Private constructor so class is uninstantiable
+     */
+    private function __construct() {
+    }
+
+    /**
+     * Get the singleton instance of this class
+     *
+     * @return TaskManager the TaskManager instance
+     */
+    public static function getInstance() {
+        if (!self::$_instance) {
+            self::$_instance = new TaskManager();
+        }
+        return self::$_instance;
+    }
+
+    /**
+     * Run all tasks that need to be run
+     */
+    public function runTasks() {
+        foreach ($this->_taskList as $task) {
+            $task = TaskFactory::getTask($task);
+            assert(is_subclass_of($task, 'AirtimeTask'));  // Sanity check
+            /** @var $task AirtimeTask */
+            if ($task && $task->shouldBeRun()) $task->run();
+        }
+    }
+
+}
+
+/**
+ * Interface AirtimeTask Interface for task operations - also acts as task type ENUM
+ */
+interface AirtimeTask {
+
+    /**
+     * PHP doesn't have ENUMs so declare them as interface constants
+     * Task types - values don't really matter as long as they're unique
+     */
+
+    const SOUNDCLOUD = "soundcloud";
+    const UPGRADE = "upgrade";
+
+    /**
+     * Check whether the task should be run
+     *
+     * @return bool true if the task needs to be run, otherwise false
+     */
+    public function shouldBeRun();
+
+    /**
+     * Run the task
+     *
+     * @return void
+     */
+    public function run();
+
+}
+
+/**
+ * Class TaskFactory Factory class to abstract task instantiation
+ */
+class TaskFactory {
+
+    /**
+     * Get an AirtimeTask based on a task type
+     *
+     * @param $task string the task type; uses AirtimeTask constants as an ENUM
+     *
+     * @return AirtimeTask|null return a task of the given type or null if no corresponding
+     *                          task exists or is implemented
+     */
+    public static function getTask($task) {
+        switch($task) {
+            case AirtimeTask::SOUNDCLOUD:
+                return new SoundcloudUploadTask();
+            case AirtimeTask::UPGRADE:
+                return new UpgradeTask();
+        }
+        return null;
+    }
+
+}
+
+/**
+ * Class UpgradeTask
+ */
+class UpgradeTask implements AirtimeTask {
+
+    /**
+     * Check the current Airtime schema version to see if an upgrade should be run
+     *
+     * @return bool true if an upgrade is needed
+     */
+    public function shouldBeRun() {
+        return UpgradeManager::checkIfUpgradeIsNeeded();
+    }
+
+    /**
+     * Run all upgrades above the current schema version
+     */
+    public function run() {
+        UpgradeManager::doUpgrade();
+    }
+
+}
+
+/**
+ * Class SoundcloudUploadTask
+ */
+class SoundcloudUploadTask implements AirtimeTask {
+
+    /**
+     * @var SoundcloudService
+     */
+    protected $_service;
+
+    public function __construct() {
+        $this->_service = new SoundcloudService();
+    }
+
+    /**
+     * Check the ThirdPartyTrackReferences table to see if there are any pending SoundCloud tasks
+     *
+     * @return bool true if there are pending tasks in ThirdPartyTrackReferences
+     */
+    public function shouldBeRun() {
+        return !$this->_service->isBrokerTaskQueueEmpty();
+    }
+
+    /**
+     * Poll the task queue for any completed Celery tasks
+     */
+    public function run() {
+        $this->_service->pollBrokerTaskQueue();
+    }
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php
index 00c37a063..2b8337e8c 100644
--- a/airtime_mvc/application/configs/constants.php
+++ b/airtime_mvc/application/configs/constants.php
@@ -83,9 +83,20 @@ define('UI_BLOCK_SESSNAME', 'BLOCK');*/
 
 
 // Soundcloud contants
-define('SOUNDCLOUD_NOT_UPLOADED_YET' , -1);
-define('SOUNDCLOUD_PROGRESS'         , -2);
-define('SOUNDCLOUD_ERROR'            , -3);
+/**
+ * @var string status string for pending Celery tasks
+ */
+define('CELERY_PENDING_STATUS', 'PENDING');
+
+/**
+ * @var string status string for successful Celery tasks
+ */
+define('CELERY_SUCCESS_STATUS', 'SUCCESS');
+
+/**
+ * @var string status string for failed Celery tasks
+ */
+define('CELERY_FAILED_STATUS', 'FAILED');
 
 
 //WHMCS integration
diff --git a/airtime_mvc/application/controllers/ErrorController.php b/airtime_mvc/application/controllers/ErrorController.php
index 0c149749d..5fd75837e 100644
--- a/airtime_mvc/application/controllers/ErrorController.php
+++ b/airtime_mvc/application/controllers/ErrorController.php
@@ -74,7 +74,6 @@ class ErrorController extends Zend_Controller_Action {
      * 404 error - route or controller
      */
     public function error404Action() {
-        Logging::info("404!");
         $this->_helper->viewRenderer('error-404');
         $this->getResponse()->setHttpResponseCode(404);
         $this->view->message = _('Page not found.');
diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index 91816901f..626cb78d6 100644
--- a/airtime_mvc/application/controllers/LibraryController.php
+++ b/airtime_mvc/application/controllers/LibraryController.php
@@ -277,13 +277,11 @@ class LibraryController extends Zend_Controller_Action
 
             $serviceId = $soundcloudService->getServiceId($id);
             if (!is_null($file) && $serviceId != 0) {
-                $menu["soundcloud"]["items"]["view"] = array("name" => _("View on Soundcloud"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/view-on-sound-cloud/id/{$id}");
-                $text = _("Re-upload to SoundCloud");
+                $menu["soundcloud"]["items"]["view"] = array("name" => _("View track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/view-on-sound-cloud/id/{$id}");
+                $menu["soundcloud"]["items"]["upload"] = array("name" => _("Remove track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/delete/id/{$id}");
             } else {
-                $text = _("Upload to SoundCloud");
+                $menu["soundcloud"]["items"]["upload"] = array("name" => _("Upload track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/upload/id/{$id}");
             }
-
-            $menu["soundcloud"]["items"]["upload"] = array("name" => $text, "icon" => "soundcloud", "url" => $baseUrl."soundcloud/upload/id/{$id}");
         }
 
         if (empty($menu)) {
diff --git a/airtime_mvc/application/controllers/SoundcloudController.php b/airtime_mvc/application/controllers/SoundcloudController.php
index c45597b87..1a7c86eb4 100644
--- a/airtime_mvc/application/controllers/SoundcloudController.php
+++ b/airtime_mvc/application/controllers/SoundcloudController.php
@@ -33,13 +33,8 @@ class SoundcloudController extends ThirdPartyController {
             $soundcloudLink = $this->_service->getLinkToFile($id);
             header('Location: ' . $soundcloudLink);
         } catch (Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
-            // If we end up here it means the track was removed from SoundCloud
-            // or the foreign id in our database is incorrect, so we should just
-            // get rid of the database record
-            Logging::warn("Error retrieving track data from SoundCloud: " . $e->getMessage());
-            $this->_service->removeTrackReference($id);
             // Redirect to a 404 so the user knows something went wrong
-            header('Location: ' . $this->_baseUrl . 'error/error-404'); // Redirect back to the Preference page
+            header('Location: ' . $this->_baseUrl . 'error/error-404');
         }
     }
 
diff --git a/airtime_mvc/application/controllers/ThirdPartyController.php b/airtime_mvc/application/controllers/ThirdPartyController.php
index 139d0c014..5c15ae5ea 100644
--- a/airtime_mvc/application/controllers/ThirdPartyController.php
+++ b/airtime_mvc/application/controllers/ThirdPartyController.php
@@ -41,6 +41,17 @@ abstract class ThirdPartyController extends Zend_Controller_Action {
         header('Location: ' . $auth_url);
     }
 
+    /**
+     * Clear the previously saved request token from the preferences
+     *
+     * @return void
+     */
+    public function deauthorizeAction() {
+        $function = $this->_SERVICE_TOKEN_ACCESSOR;
+        Application_Model_Preference::$function("");
+        header('Location: ' . $this->_baseUrl . 'Preference');  // Redirect back to the Preference page
+    }
+
     /**
      * Called when user successfully completes third-party authorization
      * Store the returned request token for future requests
@@ -67,25 +78,16 @@ abstract class ThirdPartyController extends Zend_Controller_Action {
     }
 
     /**
-     * Clear the previously saved request token from the preferences
+     * Delete the file with the given id from a third-party service
      *
      * @return void
-     */
-    public function deauthorizeAction() {
-        Application_Model_Preference::$this->_SERVICE_TOKEN_ACCESSOR("");
-        header('Location: ' . $this->_baseUrl . 'Preference');  // Redirect back to the Preference page
-    }
-
-    /**
-     * Poll the task queue for completed tasks associated with this service
-     * Optionally accepts a specific task name as a parameter
      *
-     * @return void
+     * @throws Zend_Controller_Response_Exception thrown if deletion fails for any reason
      */
-    public function pollBrokerTaskQueueAction() {
+    public function deleteAction() {
         $request = $this->getRequest();
-        $taskName = $request->getParam('task');
-        $this->_service->pollBrokerTaskQueue($taskName);
+        $id = $request->getParam('id');
+        $this->_service->delete($id);
     }
 
 }
\ No newline at end of file
diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php
index 7c26eed3d..c2bd2bfae 100644
--- a/airtime_mvc/application/controllers/UpgradeController.php
+++ b/airtime_mvc/application/controllers/UpgradeController.php
@@ -14,8 +14,7 @@ class UpgradeController extends Zend_Controller_Action
         }
 
         try {
-            $upgradeManager = new UpgradeManager();
-            $didWePerformAnUpgrade = $upgradeManager->doUpgrade();
+            $didWePerformAnUpgrade = UpgradeManager::doUpgrade();
 
             if (!$didWePerformAnUpgrade) {
                 $this->getResponse()
diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php
index 0c618861e..6f64c1643 100644
--- a/airtime_mvc/application/models/RabbitMq.php
+++ b/airtime_mvc/application/models/RabbitMq.php
@@ -9,12 +9,15 @@ class Application_Model_RabbitMq
     /**
      * @var int milliseconds (for compatibility with celery) until we consider a message to have timed out
      */
-    public static $_CELERY_MESSAGE_TIMEOUT = 300000;  // 5 minutes
+    public static $_CELERY_MESSAGE_TIMEOUT = 600000;  // 10 minutes
 
     /**
+     * We have to use celeryresults (the default results exchange) because php-celery doesn't support
+     * named results exchanges.
+     *
      * @var string exchange for celery task results
      */
-    public static $_CELERY_RESULTS_EXCHANGE = 'airtime-results';
+    public static $_CELERY_RESULTS_EXCHANGE = 'celeryresults';
 
     /**
      * Sets a flag to push the schedule at the end of the request.
@@ -90,7 +93,7 @@ class Application_Model_RabbitMq
      * @throws CeleryException when no message is found
      */
     public static function sendCeleryMessage($task, $exchange, $data) {
-        $config = parse_ini_file($this->_getRmqConfigPath(), true);
+        $config = parse_ini_file(self::_getRmqConfigPath(), true);
         $queue = $routingKey = $exchange;
         $c = self::_setupCeleryExchange($config, $exchange, $queue);  // Use the exchange name for the queue
         $result = $c->PostTask($task, $data, true, $routingKey);      // and routing key
@@ -109,8 +112,8 @@ class Application_Model_RabbitMq
      * @throws CeleryException when no message is found
      */
     public static function getAsyncResultMessage($task, $id) {
-        $config = parse_ini_file($this->_getRmqConfigPath(), true);
-        $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $config["stationId"];
+        $config = parse_ini_file(self::_getRmqConfigPath(), true);
+        $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $task;
         $c = self::_setupCeleryExchange($config, self::$_CELERY_RESULTS_EXCHANGE, $queue);
         $message = $c->getAsyncResultMessage($task, $id);
 
@@ -157,7 +160,7 @@ class Application_Model_RabbitMq
         self::sendMessage($exchange, 'direct', true, $data);
     }
 
-    private function _getRmqConfigPath() {
+    private static function _getRmqConfigPath() {
         //Hack for Airtime Pro. The RabbitMQ settings for communicating with airtime_analyzer are global
         //and shared between all instances on Airtime Pro.
         $CC_CONFIG = Config::getConfig();
@@ -177,7 +180,7 @@ class Application_Model_RabbitMq
     public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename,
                                                 $callbackUrl, $apiKey, $storageBackend, $filePrefix)
     {
-        $config = parse_ini_file($this->_getRmqConfigPath(), true);
+        $config = parse_ini_file(self::_getRmqConfigPath(), true);
         $conn = new AMQPConnection($config["rabbitmq"]["host"],
                 $config["rabbitmq"]["port"],
                 $config["rabbitmq"]["user"],
diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index 27003cac7..1745d29a8 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -17,28 +17,28 @@ class SoundcloudService extends ThirdPartyService {
     /**
      * @var string service name to store in ThirdPartyTrackReferences database
      */
-    protected $_SERVICE_NAME = 'SoundCloud';
-
-    /**
-     * @var string base URI for SoundCloud tracks
-     */
-    protected $_THIRD_PARTY_TRACK_URI = 'http://api.soundcloud.com/tracks/';
+    protected static $_SERVICE_NAME = 'SoundCloud';
 
     /**
      * @var string exchange name for SoundCloud tasks
      */
-    protected $_CELERY_EXCHANGE_NAME = 'soundcloud-uploads';
+    protected static $_CELERY_EXCHANGE_NAME = 'soundcloud';
 
     /**
      * @var string celery task name for third party uploads
      */
-    protected $_CELERY_UPLOAD_TASK_NAME = 'upload-to-soundcloud';
+    protected static $_CELERY_UPLOAD_TASK_NAME = 'soundcloud-upload';
+
+    /**
+     * @var string celery task name for third party deletions
+     */
+    protected static $_CELERY_DELETE_TASK_NAME = 'soundcloud-delete';
 
     /**
      * @var array Application_Model_Preference functions for SoundCloud and their
      *            associated API parameter keys so that we can call them dynamically
      */
-    private $_SOUNDCLOUD_PREF_FUNCTIONS = array(
+    private static $_SOUNDCLOUD_PREF_FUNCTIONS = array(
         "getDefaultSoundCloudLicenseType" => "license",
         "getDefaultSoundCloudSharingType" => "sharing"
     );
@@ -71,7 +71,7 @@ class SoundcloudService extends ThirdPartyService {
         $trackArray = array(
             'title' => $file->getName(),
         );
-        foreach ($this->_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
+        foreach (self::$_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
             $val = Application_Model_Preference::$func();
             if (!empty($val)) {
                 $trackArray[$param] = $val;
@@ -84,6 +84,7 @@ class SoundcloudService extends ThirdPartyService {
     /**
      * Update a ThirdPartyTrackReferences object for a completed upload
      * TODO: should we have a database layer class to handle Propel operations?
+     * TODO: break this function up, it's a bit of a beast
      *
      * @param $fileId int    local CcFiles identifier
      * @param $track  object third-party service track object
@@ -94,14 +95,18 @@ class SoundcloudService extends ThirdPartyService {
      */
     protected function _addOrUpdateTrackReference($fileId, $track, $status) {
         $ref = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService($this->_SERVICE_NAME)
+            ->filterByDbService(static::$_SERVICE_NAME)
             ->findOneByDbFileId($fileId);
         if (is_null($ref)) {
             $ref = new ThirdPartyTrackReferences();
+        }  // If this was a delete task, just remove the record and return
+        else if ($ref->getDbBrokerTaskName() == static::$_CELERY_DELETE_TASK_NAME) {
+            $ref->delete();
+            return;
         }
-        $ref->setDbService($this->_SERVICE_NAME);
+        $ref->setDbService(static::$_SERVICE_NAME);
         // Only set the SoundCloud fields if the task was successful
-        if ($status == $this->_SUCCESS_STATUS) {
+        if ($status == CELERY_SUCCESS_STATUS) {
             // TODO: fetch any additional SoundCloud parameters we want to store
             $ref->setDbForeignId($track->id);  // SoundCloud identifier
         }
@@ -124,12 +129,23 @@ class SoundcloudService extends ThirdPartyService {
      * @param int $fileId the local CcFiles identifier
      *
      * @return string the link to the remote file
+     *
+     * @throws Soundcloud\Exception\InvalidHttpResponseCodeException when SoundCloud returns a 4xx/5xx response
      */
     public function getLinkToFile($fileId) {
         $serviceId = $this->getServiceId($fileId);
         // If we don't find a record for the file we'll get 0 back for the id
         if ($serviceId == 0) { return ''; }
-        $track = json_decode($this->_client->get('tracks/' . $serviceId));
+        try {
+            $track = json_decode($this->_client->get('tracks/' . $serviceId));
+        } catch (Soundcloud\Exception\InvalidHttpResponseCodeException $e) {
+            // If we end up here it means the track was removed from SoundCloud
+            // or the foreign id in our database is incorrect, so we should just
+            // get rid of the database record
+            Logging::warn("Error retrieving track data from SoundCloud: " . $e->getMessage());
+            $this->removeTrackReference($fileId);
+            throw $e;  // Throw the exception up to the controller so we can redirect to a 404
+        }
         return $track->permalink_url;
     }
 
@@ -152,7 +168,7 @@ class SoundcloudService extends ThirdPartyService {
         // in the redirect. This allows us to create a singular script to redirect
         // back to any station the request comes from.
         $url = urlencode('http'.(empty($_SERVER['HTTPS'])?'':'s').'://'.$_SERVER['HTTP_HOST'].'/soundcloud/redirect');
-        return $this->_client->getAuthorizeUrl(array("state" => $url));
+        return $this->_client->getAuthorizeUrl(array("state" => $url, "scope" => "non-expiring"));
     }
 
     /**
@@ -162,7 +178,7 @@ class SoundcloudService extends ThirdPartyService {
      */
     public function requestNewAccessToken($code) {
         // Get a non-expiring access token
-        $response = $this->_client->accessToken($code, $postData = array('scope' => 'non-expiring'));
+        $response = $this->_client->accessToken($code);
         $accessToken = $response['access_token'];
         Application_Model_Preference::setSoundCloudRequestToken($accessToken);
         $this->_accessToken = $accessToken;
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
index 7ab668db4..058f3db97 100644
--- a/airtime_mvc/application/services/ThirdPartyService.php
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -1,7 +1,13 @@
 <?php
 
+/**
+ * Class ServiceNotFoundException
+ */
+class ServiceNotFoundException extends Exception {}
+
 /**
  * Class ThirdPartyService generic superclass for third-party services
+ * TODO: decouple the media/track-specific functions into ThirdPartyMediaService class?
  */
 abstract class ThirdPartyService {
 
@@ -13,44 +19,32 @@ abstract class ThirdPartyService {
     /**
      * @var string service name to store in ThirdPartyTrackReferences database
      */
-    protected $_SERVICE_NAME;
+    protected static $_SERVICE_NAME;
 
     /**
      * @var string base URI for third-party tracks
      */
-    protected $_THIRD_PARTY_TRACK_URI;
+    protected static $_THIRD_PARTY_TRACK_URI;
 
     /**
      * @var string broker exchange name for third party tasks
      */
-    protected $_CELERY_EXCHANGE_NAME = 'default';
+    protected static $_CELERY_EXCHANGE_NAME;
 
     /**
      * @var string celery task name for third party uploads
      */
-    protected $_CELERY_UPLOAD_TASK_NAME = 'upload';
+    protected static $_CELERY_UPLOAD_TASK_NAME;
 
     /**
-     * @var string status string for pending tasks
+     * @var string celery task name for third party deletion
      */
-    protected $_PENDING_STATUS = 'PENDING';
-
-    /**
-     * @var string status string for successful tasks
-     */
-    protected $_SUCCESS_STATUS = 'SUCCESS';
-
-    /**
-     * @var string status string for failed tasks
-     */
-    protected $_FAILED_STATUS = 'FAILED';
+    protected static $_CELERY_DELETE_TASK_NAME;
 
     /**
      * Upload the file with the given identifier to a third-party service
      *
      * @param int $fileId the local CcFiles identifier
-     *
-     * @throws Exception thrown when the upload fails for any reason
      */
     public function upload($fileId) {
         $file = Application_Model_StoredFile::RecallById($fileId);
@@ -60,11 +54,40 @@ abstract class ThirdPartyService {
             'file_path' => $file->getFilePaths()[0]
         );
         try {
-            $brokerTaskId = Application_Model_RabbitMq::sendCeleryMessage($this->_CELERY_UPLOAD_TASK_NAME,
-                                                                          $this->_CELERY_EXCHANGE_NAME,
+            $brokerTaskId = Application_Model_RabbitMq::sendCeleryMessage(static::$_CELERY_UPLOAD_TASK_NAME,
+                                                                          static::$_CELERY_EXCHANGE_NAME,
                                                                           $data);
-            $this->_createTaskReference($fileId, $brokerTaskId, $this->_CELERY_UPLOAD_TASK_NAME);
-        } catch(Exception $e) {
+            $this->_createTaskReference($fileId, $brokerTaskId, static::$_CELERY_UPLOAD_TASK_NAME);
+        } catch (Exception $e) {
+            Logging::info("Invalid request: " . $e->getMessage());
+            // We should only get here if we have an access token, so attempt to refresh
+            $this->accessTokenRefresh();
+        }
+    }
+
+    /**
+     * Delete the file with the given identifier from a third-party service
+     *
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @throws ServiceNotFoundException when a $fileId with no corresponding
+     *                                  service identifier is given
+     */
+    public function delete($fileId) {
+        $serviceId = $this->getServiceId($fileId);
+        if ($serviceId == 0) {
+            throw new ServiceNotFoundException("No service found for file with ID $fileId");
+        }
+        $data = array(
+            'token' => $this->_accessToken,
+            'track_id' => $serviceId
+        );
+        try {
+            $brokerTaskId = Application_Model_RabbitMq::sendCeleryMessage(static::$_CELERY_DELETE_TASK_NAME,
+                                                                          static::$_CELERY_EXCHANGE_NAME,
+                                                                          $data);
+            $this->_createTaskReference($fileId, $brokerTaskId, static::$_CELERY_DELETE_TASK_NAME);
+        } catch (Exception $e) {
             Logging::info("Invalid request: " . $e->getMessage());
             // We should only get here if we have an access token, so attempt to refresh
             $this->accessTokenRefresh();
@@ -86,18 +109,18 @@ abstract class ThirdPartyService {
     protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
         // First, check if the track already has an entry in the database
         $ref = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService($this->_SERVICE_NAME)
+            ->filterByDbService(static::$_SERVICE_NAME)
             ->findOneByDbFileId($fileId);
         if (is_null($ref)) {
             $ref = new ThirdPartyTrackReferences();
         }
-        $ref->setDbService($this->_SERVICE_NAME);
+        $ref->setDbService(static::$_SERVICE_NAME);
         $ref->setDbBrokerTaskId($brokerTaskId);
         $ref->setDbBrokerTaskName($taskName);
         $utc = new DateTimeZone("UTC");
         $ref->setDbBrokerTaskDispatchTime(new DateTime("now", $utc));
         $ref->setDbFileId($fileId);
-        $ref->setDbStatus($this->_PENDING_STATUS);
+        $ref->setDbStatus(CELERY_PENDING_STATUS);
         $ref->save();
     }
 
@@ -113,7 +136,7 @@ abstract class ThirdPartyService {
      */
     public function removeTrackReference($fileId) {
         $ref = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService($this->_SERVICE_NAME)
+            ->filterByDbService(static::$_SERVICE_NAME)
             ->findOneByDbFileId($fileId);
         $ref->delete();
     }
@@ -128,9 +151,9 @@ abstract class ThirdPartyService {
      */
     public function getServiceId($fileId) {
         $ref = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService($this->_SERVICE_NAME)
-            ->findOneByDbFileId($fileId); // There shouldn't be duplicates!
-        return is_null($ref) ? 0 : $ref->getDbForeignId();
+            ->filterByDbService(static::$_SERVICE_NAME)
+            ->findOneByDbFileId($fileId);  // There shouldn't be duplicates!
+        return empty($ref) ? 0 : $ref->getDbForeignId();
     }
 
     /**
@@ -143,7 +166,24 @@ abstract class ThirdPartyService {
      */
     public function getLinkToFile($fileId) {
         $serviceId = $this->getServiceId($fileId);
-        return $serviceId > 0 ? $this->_THIRD_PARTY_TRACK_URI . $serviceId : '';
+        return $serviceId > 0 ? static::$_THIRD_PARTY_TRACK_URI . $serviceId : '';
+    }
+
+    /**
+     * Check to see if there are any pending tasks for this service
+     *
+     * @param string $taskName
+     *
+     * @return bool true if there are any pending tasks, otherwise false
+     */
+    public function isBrokerTaskQueueEmpty($taskName="") {
+        $query = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService(static::$_SERVICE_NAME);
+        if (!empty($taskName)) {
+            $query->filterByDbBrokerTaskName($taskName);
+        }
+        $result = $query->findOneByDbStatus(CELERY_PENDING_STATUS);
+        return empty($result);
     }
 
     /**
@@ -154,18 +194,22 @@ abstract class ThirdPartyService {
      * @param string $taskName the name of the task to poll for
      */
     public function pollBrokerTaskQueue($taskName="") {
-        $pendingTasks = $this->_getPendingTasks($taskName);
+        $pendingTasks = static::_getPendingTasks($taskName);
         foreach ($pendingTasks as $task) {
             try {
-                $message = $this->_getTaskMessage($task);
-                $this->_addOrUpdateTrackReference($task->getDbFileId(), json_decode($message->result), $message->status);
-            } catch(CeleryException $e) {
-                Logging::info("Couldn't retrieve task message for task " . $task->getDbBrokerTaskName()
-                              . " with ID " . $task->getDbBrokerTaskId() . ": " . $e->getMessage());
-                if ($this->_checkMessageTimeout($task)) {
-                    $task->setDbStatus($this->_FAILED_STATUS);
+                $message = static::_getTaskMessage($task);
+                static::_addOrUpdateTrackReference($task->getDbFileId(), json_decode($message->result), $message->status);
+            } catch (CeleryException $e) {
+                // Fail silently unless the message has timed out; often we end up here when
+                // the Celery task takes a while to execute
+                if (static::_checkMessageTimeout($task)) {
+                    Logging::info($e->getMessage());
+                    $task->setDbStatus(CELERY_FAILED_STATUS);
                     $task->save();
                 }
+            } catch (Exception $e) {
+                // Sometimes we might catch a json_decode error and end up here
+                Logging::info($e->getMessage());
             }
         }
     }
@@ -180,8 +224,8 @@ abstract class ThirdPartyService {
      */
     protected function _getPendingTasks($taskName) {
         $query = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService($this->_SERVICE_NAME)
-            ->filterByDbStatus($this->_PENDING_STATUS)
+            ->filterByDbService(static::$_SERVICE_NAME)
+            ->filterByDbStatus(CELERY_PENDING_STATUS)
             ->filterByDbBrokerTaskId('', Criteria::NOT_EQUAL);
         if (!empty($taskName)) {
             $query->filterByDbBrokerTaskName($taskName);
@@ -198,7 +242,7 @@ abstract class ThirdPartyService {
      *
      * @throws CeleryException when the result message for this task no longer exists
      */
-    protected function _getTaskMessage($task) {
+    protected static function _getTaskMessage($task) {
         $message =  Application_Model_RabbitMq::getAsyncResultMessage($task->getDbBrokerTaskName(),
                                                                       $task->getDbBrokerTaskId());
         return json_decode($message['body']);
@@ -212,7 +256,7 @@ abstract class ThirdPartyService {
      * @return bool true if the dispatch time is empty or it's been more than our timeout time
      *              since the message was dispatched, otherwise false
      */
-    protected function _checkMessageTimeout($task) {
+    protected static function _checkMessageTimeout($task) {
         $utc = new DateTimeZone("UTC");
         $dispatchTime = new DateTime($task->getDbBrokerTaskDispatchTime(), $utc);
         $now = new DateTime("now", $utc);
diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 21cad0e71..7510e6c52 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -49,7 +49,7 @@ class UpgradeManager
      *
      * @return boolean whether or not an upgrade was performed
      */
-    public function doUpgrade()
+    public static function doUpgrade()
     {
         // Get all upgrades dynamically (in declaration order!) so we don't have to add them explicitly each time
         // TODO: explicitly sort classnames by ascending version suffix for safety
@@ -58,7 +58,7 @@ class UpgradeManager
         $upgradePerformed = false;
 
         foreach ($upgraders as $upgrader) {
-            $upgradePerformed = $this->_runUpgrade(new $upgrader($dir)) ? true : $upgradePerformed;
+            $upgradePerformed = self::_runUpgrade(new $upgrader($dir)) ? true : $upgradePerformed;
         }
 
         return $upgradePerformed;
@@ -71,7 +71,7 @@ class UpgradeManager
      *
      * @return bool true if the upgrade was successful, otherwise false
      */
-    private function _runUpgrade(AirtimeUpgrader $upgrader) {
+    private static function _runUpgrade(AirtimeUpgrader $upgrader) {
         return $upgrader->checkIfUpgradeSupported() && $upgrader->upgrade();
     }
 
@@ -327,6 +327,22 @@ class AirtimeUpgrader2512 extends AirtimeUpgrader
     }
 }
 
+/**
+ * Class AirtimeUpgrader2513 - Celery and SoundCloud upgrade
+ *
+ * Adds third_party_track_references table for third party service
+ * authentication and task architecture.
+ *
+ * Schema:
+ *      id                          -> int          PK
+ *      service                     -> string       internal service name
+ *      foreign_id                  -> int          external unique service id
+ *      broker_task_id              -> int          external unique amqp results identifier
+ *      broker_task_name            -> string       external Celery task name
+ *      broker_task_dispatch_time   -> timestamp    internal message dispatch time
+ *      file_id                     -> int          internal FK->cc_files track id
+ *      status                      -> string       external Celery task status - PENDING, SUCCESS, or FAILED
+ */
 class AirtimeUpgrader2513 extends AirtimeUpgrader
 {
     protected function getSupportedSchemaVersions() {
diff --git a/airtime_mvc/public/js/airtime/common/common.js b/airtime_mvc/public/js/airtime/common/common.js
index 2cb0501fe..48efc474b 100644
--- a/airtime_mvc/public/js/airtime/common/common.js
+++ b/airtime_mvc/public/js/airtime/common/common.js
@@ -9,7 +9,6 @@ $(document).ready(function() {
 
     //this statement tells the browser to fade out any success message after 5 seconds
     setTimeout(function(){$(".success").fadeOut("slow", function(){$(this).empty()});}, 5000);
-    pollTaskQueues();
 });
 
 /*
@@ -156,9 +155,4 @@ function removeSuccessMsg() {
     var $status = $('.success');
     
     $status.fadeOut("slow", function(){$status.empty()});
-}
-
-function pollTaskQueues() {
-    console.log("Polling broker queues...");
-    $.get(baseUrl + 'soundcloud/poll-broker-task-queue');
 }
\ No newline at end of file
diff --git a/python_apps/airtime-celery/airtime-celery/celeryconfig.py b/python_apps/airtime-celery/airtime-celery/celeryconfig.py
index cb986d0f9..da43f1e77 100644
--- a/python_apps/airtime-celery/airtime-celery/celeryconfig.py
+++ b/python_apps/airtime-celery/airtime-celery/celeryconfig.py
@@ -23,23 +23,15 @@ def parse_rmq_config(rmq_config):
 
 # Celery amqp settings
 BROKER_URL = get_rmq_broker()
-CELERY_RESULT_BACKEND = 'amqp'     # Use RabbitMQ as the celery backend
-CELERY_RESULT_PERSISTENT = True    # Persist through a broker restart
-CELERY_TASK_RESULT_EXPIRES = 300   # Expire task results after 5 minutes
-CELERY_TRACK_STARTED = False
-CELERY_RESULT_EXCHANGE = 'airtime-results'
+CELERY_RESULT_BACKEND = 'amqp'            # Use RabbitMQ as the celery backend
+CELERY_RESULT_PERSISTENT = True           # Persist through a broker restart
+CELERY_TASK_RESULT_EXPIRES = 600          # Expire task results after 10 minutes
+CELERY_RESULT_EXCHANGE = 'celeryresults'  # Default exchange - needed due to php-celery
 CELERY_QUEUES = (
-    Queue('soundcloud-uploads', exchange=Exchange('soundcloud-uploads'), routing_key='soundcloud-uploads'),
-    Queue('airtime-results.soundcloud-uploads', exchange=Exchange('airtime-results')),
-)
-CELERY_ROUTES = (
-    {
-        'soundcloud_uploads.tasks.upload_to_soundcloud': {
-            'exchange': 'airtime-results',
-            'queue': 'airtime-results.soundcloud-uploads',
-        }
-    },
+    Queue('soundcloud', exchange=Exchange('soundcloud'), routing_key='soundcloud'),
+    Queue(exchange=Exchange('celeryresults'), auto_delete=True),
 )
+CELERY_EVENT_QUEUE_EXPIRES = 600          # RabbitMQ x-expire after 10 minutes
 
 # Celery task settings
 CELERY_TASK_SERIALIZER = 'json'
diff --git a/python_apps/airtime-celery/airtime-celery/tasks.py b/python_apps/airtime-celery/airtime-celery/tasks.py
index 76009a75f..bfe873609 100644
--- a/python_apps/airtime-celery/airtime-celery/tasks.py
+++ b/python_apps/airtime-celery/airtime-celery/tasks.py
@@ -9,8 +9,8 @@ celery = Celery()
 logger = get_task_logger(__name__)
 
 
-@celery.task(name='upload-to-soundcloud')
-def upload_to_soundcloud(data, token, file_path):
+@celery.task(name='soundcloud-upload')
+def soundcloud_upload(data, token, file_path):
     """
     Upload a file to SoundCloud
 
@@ -32,3 +32,22 @@ def upload_to_soundcloud(data, token, file_path):
         raise e
     data['asset_data'].close()
     return json.dumps(track.fields())
+
+@celery.task(name='soundcloud-delete')
+def soundcloud_delete(token, track_id):
+    """
+    Delete a file from SoundCloud
+
+    :param token: OAuth2 client access token
+
+    :return: the SoundCloud response object
+    :rtype: dict
+    """
+    client = soundcloud.Client(access_token=token)
+    try:
+        logger.info('Deleting track with ID {0}'.format(track_id))
+        track = client.delete('/tracks/%s' % track_id)
+    except Exception as e:
+        logger.info('Error deleting track!')
+        raise e
+    return json.dumps(track.fields())

From 8d2e476ff1cfde998874ef653d39f94d69c1726e Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 16 Jun 2015 15:10:08 -0400
Subject: [PATCH 10/39] SAAS-868 - Refactor third party + celery workflow,
 implement locking on TaskManager

---
 airtime_mvc/application/Bootstrap.php         |    2 +
 .../application/common/TaskManager.php        |   93 +-
 .../application/common/interface/OAuth2.php   |   31 +
 .../configs/classmap-airtime-conf.php         |    7 +
 airtime_mvc/application/configs/constants.php |   26 +-
 .../upgrade_sql/airtime_2.5.13/upgrade.sql    |   32 +-
 airtime_mvc/application/models/Preference.php |   10 +
 airtime_mvc/application/models/RabbitMq.php   |   84 +-
 .../models/airtime/CeleryTasks.php            |   18 +
 .../models/airtime/CeleryTasksPeer.php        |   18 +
 .../models/airtime/CeleryTasksQuery.php       |   18 +
 .../models/airtime/map/CcFilesTableMap.php    |    1 +
 .../map/CcPlayoutHistoryTemplateTableMap.php  |    1 -
 .../airtime/map/CeleryTasksTableMap.php       |   57 +
 .../map/ThirdPartyTrackReferencesTableMap.php |   11 +-
 .../models/airtime/om/BaseCcFiles.php         |  285 +++++
 .../models/airtime/om/BaseCcFilesPeer.php     |    3 +
 .../models/airtime/om/BaseCcFilesQuery.php    |   78 ++
 .../om/BaseCcPlayoutHistoryTemplate.php       |  285 -----
 .../om/BaseCcPlayoutHistoryTemplatePeer.php   |    3 -
 .../om/BaseCcPlayoutHistoryTemplateQuery.php  |   78 --
 .../models/airtime/om/BaseCeleryTasks.php     | 1119 +++++++++++++++++
 .../models/airtime/om/BaseCeleryTasksPeer.php | 1010 +++++++++++++++
 .../airtime/om/BaseCeleryTasksQuery.php       |  504 ++++++++
 .../om/BaseThirdPartyTrackReferences.php      |  602 ++++++---
 .../om/BaseThirdPartyTrackReferencesPeer.php  |   95 +-
 .../om/BaseThirdPartyTrackReferencesQuery.php |  282 +++--
 .../application/services/CeleryService.php    |  206 +++
 .../services/CeleryServiceFactory.php         |   20 +
 .../services/SoundcloudService.php            |   34 +-
 .../services/ThirdPartyCeleryService.php      |  135 ++
 .../services/ThirdPartyService.php            |  241 +---
 airtime_mvc/build/schema.xml                  |   25 +-
 airtime_mvc/build/sql/schema.sql              |   31 +-
 34 files changed, 4302 insertions(+), 1143 deletions(-)
 create mode 100644 airtime_mvc/application/common/interface/OAuth2.php
 create mode 100644 airtime_mvc/application/models/airtime/CeleryTasks.php
 create mode 100644 airtime_mvc/application/models/airtime/CeleryTasksPeer.php
 create mode 100644 airtime_mvc/application/models/airtime/CeleryTasksQuery.php
 create mode 100644 airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php
 create mode 100644 airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php
 create mode 100644 airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php
 create mode 100644 airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php
 create mode 100644 airtime_mvc/application/services/CeleryService.php
 create mode 100644 airtime_mvc/application/services/CeleryServiceFactory.php
 create mode 100644 airtime_mvc/application/services/ThirdPartyCeleryService.php

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 7f0bc7d7d..94acbc5db 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -27,7 +27,9 @@ require_once "ProvisioningHelper.php";
 require_once "GoogleAnalytics.php";
 require_once "Timezone.php";
 require_once "Auth.php";
+require_once "interface/OAuth2.php";
 require_once "TaskManager.php";
+require_once __DIR__.'/services/CeleryService.php';
 require_once __DIR__.'/services/SoundcloudService.php';
 require_once __DIR__.'/forms/helpers/ValidationTypes.php';
 require_once __DIR__.'/forms/helpers/CustomDecorators.php';
diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php
index a52bfa063..21b2b073f 100644
--- a/airtime_mvc/application/common/TaskManager.php
+++ b/airtime_mvc/application/common/TaskManager.php
@@ -9,8 +9,8 @@ final class TaskManager {
      * @var array tasks to be run
      */
     protected $_taskList = [
-        AirtimeTask::SOUNDCLOUD,
-        AirtimeTask::UPGRADE
+        AirtimeTask::UPGRADE,  // Always run the upgrade first
+        AirtimeTask::CELERY
     ];
 
     /**
@@ -18,6 +18,17 @@ final class TaskManager {
      */
     protected static $_instance;
 
+    /**
+     * @var int TASK_INTERVAL_SECONDS how often, in seconds, to run the TaskManager tasks,
+     *                                if they need to be run
+     */
+    const TASK_INTERVAL_SECONDS = 60;
+
+    /**
+     * @var $con PDO Propel connection object
+     */
+    private $_con;
+
     /**
      * Private constructor so class is uninstantiable
      */
@@ -40,14 +51,63 @@ final class TaskManager {
      * Run all tasks that need to be run
      */
     public function runTasks() {
+        // If there is data in auth storage, this could be a user request
+        // so we should lock the TaskManager to avoid blocking
+        if ($this->_isUserSessionRequest()) return;
+        $this->_con = Propel::getConnection(CcPrefPeer::DATABASE_NAME);
+        $this->_con->beginTransaction();
+        try {
+            $lock = $this->_getLock();
+            if ($lock && microtime(true) < $lock['valstr'] + self::TASK_INTERVAL_SECONDS) return;
+            $this->_updateLock($lock);
+            $this->_con->commit();
+        } catch (Exception $e) {
+            // We get here if there are simultaneous requests trying to fetch the lock row
+            $this->_con->rollBack();
+            Logging::info($e->getMessage());
+            return;
+        }
         foreach ($this->_taskList as $task) {
             $task = TaskFactory::getTask($task);
-            assert(is_subclass_of($task, 'AirtimeTask'));  // Sanity check
-            /** @var $task AirtimeTask */
             if ($task && $task->shouldBeRun()) $task->run();
         }
     }
 
+    /**
+     * Check if the current session is a user request
+     *
+     * @return bool
+     */
+    private function _isUserSessionRequest() {
+        $auth = Zend_Auth::getInstance();
+        $data = $auth->getStorage()->read();
+        return !empty($data);
+    }
+
+    /**
+     * Get the task_manager_lock from cc_pref with a row-level lock for atomicity
+     *
+     * @return array|bool an array containing the row values, or false on failure
+     */
+    private function _getLock() {
+        $sql = "SELECT * FROM cc_pref WHERE keystr='task_manager_lock' LIMIT 1 FOR UPDATE NOWAIT";
+        $st = $this->_con->prepare($sql);
+        $st->execute();
+        return $st->fetch();
+    }
+
+    /**
+     * Update and commit the new lock value, or insert it if it doesn't exist
+     *
+     * @param $lock array cc_pref lock row values
+     */
+    private function _updateLock($lock) {
+        $sql = empty($lock) ? "INSERT INTO cc_pref (keystr, valstr) VALUES ('task_manager_lock', :value)"
+            : "UPDATE cc_pref SET valstr=:value WHERE keystr='task_manager_lock'";
+        $st = $this->_con->prepare($sql);
+        $st->execute(array(":value" => microtime(true)));
+    }
+
 }
 
 /**
@@ -60,8 +120,8 @@ interface AirtimeTask {
      * Task types - values don't really matter as long as they're unique
      */
 
-    const SOUNDCLOUD = "soundcloud";
     const UPGRADE = "upgrade";
+    const CELERY = "celery";
 
     /**
      * Check whether the task should be run
@@ -94,10 +154,10 @@ class TaskFactory {
      */
     public static function getTask($task) {
         switch($task) {
-            case AirtimeTask::SOUNDCLOUD:
-                return new SoundcloudUploadTask();
             case AirtimeTask::UPGRADE:
                 return new UpgradeTask();
+            case AirtimeTask::CELERY:
+                return new CeleryTask();
         }
         return null;
     }
@@ -128,33 +188,24 @@ class UpgradeTask implements AirtimeTask {
 }
 
 /**
- * Class SoundcloudUploadTask
+ * Class CeleryTask
  */
-class SoundcloudUploadTask implements AirtimeTask {
+class CeleryTask implements AirtimeTask {
 
     /**
-     * @var SoundcloudService
-     */
-    protected $_service;
-
-    public function __construct() {
-        $this->_service = new SoundcloudService();
-    }
-
-    /**
-     * Check the ThirdPartyTrackReferences table to see if there are any pending SoundCloud tasks
+     * Check the ThirdPartyTrackReferences table to see if there are any pending tasks
      *
      * @return bool true if there are pending tasks in ThirdPartyTrackReferences
      */
     public function shouldBeRun() {
-        return !$this->_service->isBrokerTaskQueueEmpty();
+        return !CeleryService::isBrokerTaskQueueEmpty();
     }
 
     /**
      * Poll the task queue for any completed Celery tasks
      */
     public function run() {
-        $this->_service->pollBrokerTaskQueue();
+        CeleryService::pollBrokerTaskQueue();
     }
 
 }
\ No newline at end of file
diff --git a/airtime_mvc/application/common/interface/OAuth2.php b/airtime_mvc/application/common/interface/OAuth2.php
new file mode 100644
index 000000000..28283c8e8
--- /dev/null
+++ b/airtime_mvc/application/common/interface/OAuth2.php
@@ -0,0 +1,31 @@
+<?php
+
+interface OAuth2 {
+
+    /**
+     * Check whether an OAuth access token exists
+     *
+     * @return bool true if an access token exists, otherwise false
+     */
+    public function hasAccessToken();
+
+    /**
+     * Get the OAuth authorization URL
+     *
+     * @return string the authorization URL
+     */
+    public function getAuthorizeUrl();
+
+    /**
+     * Request a new OAuth access token and store it in CcPref
+     *
+     * @param $code string exchange authorization code for access token
+     */
+    public function requestNewAccessToken($code);
+
+    /**
+     * Regenerate the OAuth access token
+     */
+    public function accessTokenRefresh();
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/configs/classmap-airtime-conf.php b/airtime_mvc/application/configs/classmap-airtime-conf.php
index d6f5159e2..671726a17 100644
--- a/airtime_mvc/application/configs/classmap-airtime-conf.php
+++ b/airtime_mvc/application/configs/classmap-airtime-conf.php
@@ -100,6 +100,9 @@ return array (
   'BaseCcWebstreamMetadataQuery' => 'airtime/om/BaseCcWebstreamMetadataQuery.php',
   'BaseCcWebstreamPeer' => 'airtime/om/BaseCcWebstreamPeer.php',
   'BaseCcWebstreamQuery' => 'airtime/om/BaseCcWebstreamQuery.php',
+  'BaseCeleryTasks' => 'airtime/om/BaseCeleryTasks.php',
+  'BaseCeleryTasksPeer' => 'airtime/om/BaseCeleryTasksPeer.php',
+  'BaseCeleryTasksQuery' => 'airtime/om/BaseCeleryTasksQuery.php',
   'BaseCloudFile' => 'airtime/om/BaseCloudFile.php',
   'BaseCloudFilePeer' => 'airtime/om/BaseCloudFilePeer.php',
   'BaseCloudFileQuery' => 'airtime/om/BaseCloudFileQuery.php',
@@ -238,6 +241,10 @@ return array (
   'CcWebstreamPeer' => 'airtime/CcWebstreamPeer.php',
   'CcWebstreamQuery' => 'airtime/CcWebstreamQuery.php',
   'CcWebstreamTableMap' => 'airtime/map/CcWebstreamTableMap.php',
+  'CeleryTasks' => 'airtime/CeleryTasks.php',
+  'CeleryTasksPeer' => 'airtime/CeleryTasksPeer.php',
+  'CeleryTasksQuery' => 'airtime/CeleryTasksQuery.php',
+  'CeleryTasksTableMap' => 'airtime/map/CeleryTasksTableMap.php',
   'CloudFile' => 'airtime/CloudFile.php',
   'CloudFilePeer' => 'airtime/CloudFilePeer.php',
   'CloudFileQuery' => 'airtime/CloudFileQuery.php',
diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php
index 2b8337e8c..33247cbcc 100644
--- a/airtime_mvc/application/configs/constants.php
+++ b/airtime_mvc/application/configs/constants.php
@@ -81,24 +81,6 @@ define('UI_PLAYLISTCONTROLLER_OBJ_SESSNAME', 'PLAYLISTCONTROLLER_OBJ');
 /*define('UI_PLAYLIST_SESSNAME', 'PLAYLIST');
 define('UI_BLOCK_SESSNAME', 'BLOCK');*/
 
-
-// Soundcloud contants
-/**
- * @var string status string for pending Celery tasks
- */
-define('CELERY_PENDING_STATUS', 'PENDING');
-
-/**
- * @var string status string for successful Celery tasks
- */
-define('CELERY_SUCCESS_STATUS', 'SUCCESS');
-
-/**
- * @var string status string for failed Celery tasks
- */
-define('CELERY_FAILED_STATUS', 'FAILED');
-
-
 //WHMCS integration
 define("WHMCS_API_URL", "https://account.sourcefabric.com/includes/api.php");
 define("SUBDOMAIN_WHMCS_CUSTOM_FIELD_NAME", "Choose your domain");
@@ -113,3 +95,11 @@ define('PROVISIONING_STATUS_ACTIVE' , 'Active');
 //TuneIn integration
 define("TUNEIN_API_URL", "http://air.radiotime.com/Playing.ashx");
 
+// Celery
+define('CELERY_PENDING_STATUS', 'PENDING');
+define('CELERY_SUCCESS_STATUS', 'SUCCESS');
+define('CELERY_FAILED_STATUS', 'FAILED');
+
+// Celery Services
+define('SOUNDCLOUD_SERVICE_NAME', 'soundcloud');
+
diff --git a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
index 15a6432d7..fbf7235bf 100644
--- a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
+++ b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
@@ -1,19 +1,41 @@
+-----------------------------------------------------------------------
+-- third_party_track_references
+-----------------------------------------------------------------------
+
 CREATE TABLE IF NOT EXISTS "third_party_track_references"
 (
     "id" serial NOT NULL,
     "service" VARCHAR(256) NOT NULL,
     "foreign_id" VARCHAR(256),
-    "broker_task_id" VARCHAR(256),
-    "broker_task_name" VARCHAR(256),
-    "broker_task_dispatch_time" TIMESTAMP,
     "file_id" INTEGER NOT NULL,
-    "status" VARCHAR(256) NOT NULL,
+    "upload_time" TIMESTAMP,
+    "status" VARCHAR(256),
     PRIMARY KEY ("id"),
-    CONSTRAINT "broker_task_id_unique" UNIQUE ("broker_task_id"),
     CONSTRAINT "foreign_id_unique" UNIQUE ("foreign_id")
 );
 
+-----------------------------------------------------------------------
+-- celery_tasks
+-----------------------------------------------------------------------
+
+CREATE TABLE IF NOT EXISTS "celery_tasks"
+(
+    "id" VARCHAR(256) NOT NULL,
+    "track_reference" INTEGER NOT NULL,
+    "name" VARCHAR(256),
+    "dispatch_time" TIMESTAMP,
+    "status" VARCHAR(256) NOT NULL,
+    PRIMARY KEY ("id"),
+    CONSTRAINT "id_unique" UNIQUE ("id")
+);
+
+
 ALTER TABLE "third_party_track_references" ADD CONSTRAINT "track_reference_fkey"
     FOREIGN KEY ("file_id")
     REFERENCES "cc_files" ("id")
     ON DELETE CASCADE;
+
+ALTER TABLE "celery_tasks" ADD CONSTRAINT "celery_service_fkey"
+    FOREIGN KEY ("track_reference")
+    REFERENCES "third_party_track_references" ("id")
+    ON DELETE CASCADE;
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index 0ac3c199a..5bb478e7b 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -1461,4 +1461,14 @@ class Application_Model_Preference
         self::setValue("soundcloud_request_token", $value);
     }
 
+    // TaskManager Lock Timestamp
+
+    public static function getTaskManagerLock() {
+        return self::getValue("task_manager_lock");
+    }
+
+    public static function setTaskManagerLock($value) {
+        self::setValue("task_manager_lock", $value);
+    }
+
 }
diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php
index 6f64c1643..87a89336d 100644
--- a/airtime_mvc/application/models/RabbitMq.php
+++ b/airtime_mvc/application/models/RabbitMq.php
@@ -6,19 +6,6 @@ class Application_Model_RabbitMq
 {
     public static $doPush = false;
 
-    /**
-     * @var int milliseconds (for compatibility with celery) until we consider a message to have timed out
-     */
-    public static $_CELERY_MESSAGE_TIMEOUT = 600000;  // 10 minutes
-
-    /**
-     * We have to use celeryresults (the default results exchange) because php-celery doesn't support
-     * named results exchanges.
-     *
-     * @var string exchange for celery task results
-     */
-    public static $_CELERY_RESULTS_EXCHANGE = 'celeryresults';
-
     /**
      * Sets a flag to push the schedule at the end of the request.
      */
@@ -56,73 +43,6 @@ class Application_Model_RabbitMq
         $conn->close();
     }
 
-    /**
-     * Connect to the Celery daemon via amqp
-     *
-     * @param $config   array  the airtime configuration array
-     * @param $exchange string the amqp exchange name
-     * @param $queue    string the amqp queue name
-     *
-     * @return Celery the Celery connection object
-     *
-     * @throws Exception when a connection error occurs
-     */
-    private static function _setupCeleryExchange($config, $exchange, $queue) {
-        return new Celery($config["rabbitmq"]["host"],
-                          $config["rabbitmq"]["user"],
-                          $config["rabbitmq"]["password"],
-                          $config["rabbitmq"]["vhost"],
-                          $exchange,                        // Exchange name
-                          $queue,                           // Binding/queue
-                          $config["rabbitmq"]["port"],
-                          false,                            // Connector
-                          true,                             // Persistent messages
-                          self::$_CELERY_MESSAGE_TIMEOUT);  // Result expiration
-    }
-
-    /**
-     * Send an amqp message to Celery the airtime-celery daemon to perform a task
-     *
-     * @param $task     string the Celery task name
-     * @param $exchange string the amqp exchange name
-     * @param $data     array  an associative array containing arguments for the Celery task
-     *
-     * @return string the task identifier for the started Celery task so we can fetch the
-     *                results asynchronously later
-     *
-     * @throws CeleryException when no message is found
-     */
-    public static function sendCeleryMessage($task, $exchange, $data) {
-        $config = parse_ini_file(self::_getRmqConfigPath(), true);
-        $queue = $routingKey = $exchange;
-        $c = self::_setupCeleryExchange($config, $exchange, $queue);  // Use the exchange name for the queue
-        $result = $c->PostTask($task, $data, true, $routingKey);      // and routing key
-        return $result->getId();
-    }
-
-    /**
-     * Given a task name and identifier, check the Celery results queue for any
-     * corresponding messages
-     *
-     * @param $task string the Celery task name
-     * @param $id   string the Celery task identifier
-     *
-     * @return object the message object
-     *
-     * @throws CeleryException when no message is found
-     */
-    public static function getAsyncResultMessage($task, $id) {
-        $config = parse_ini_file(self::_getRmqConfigPath(), true);
-        $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $task;
-        $c = self::_setupCeleryExchange($config, self::$_CELERY_RESULTS_EXCHANGE, $queue);
-        $message = $c->getAsyncResultMessage($task, $id);
-
-        if ($message == FALSE) {
-            throw new CeleryException("Failed to get message for task $task with ID $id");
-        }
-        return $message;
-    }
-
     public static function SendMessageToPypo($event_type, $md)
     {
         $md["event_type"] = $event_type;
@@ -160,7 +80,7 @@ class Application_Model_RabbitMq
         self::sendMessage($exchange, 'direct', true, $data);
     }
 
-    private static function _getRmqConfigPath() {
+    public static function getRmqConfigPath() {
         //Hack for Airtime Pro. The RabbitMQ settings for communicating with airtime_analyzer are global
         //and shared between all instances on Airtime Pro.
         $CC_CONFIG = Config::getConfig();
@@ -180,7 +100,7 @@ class Application_Model_RabbitMq
     public static function SendMessageToAnalyzer($tmpFilePath, $importedStorageDirectory, $originalFilename,
                                                 $callbackUrl, $apiKey, $storageBackend, $filePrefix)
     {
-        $config = parse_ini_file(self::_getRmqConfigPath(), true);
+        $config = parse_ini_file(self::getRmqConfigPath(), true);
         $conn = new AMQPConnection($config["rabbitmq"]["host"],
                 $config["rabbitmq"]["port"],
                 $config["rabbitmq"]["user"],
diff --git a/airtime_mvc/application/models/airtime/CeleryTasks.php b/airtime_mvc/application/models/airtime/CeleryTasks.php
new file mode 100644
index 000000000..f91e22a5d
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/CeleryTasks.php
@@ -0,0 +1,18 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for representing a row from the 'celery_tasks' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.airtime
+ */
+class CeleryTasks extends BaseCeleryTasks
+{
+}
diff --git a/airtime_mvc/application/models/airtime/CeleryTasksPeer.php b/airtime_mvc/application/models/airtime/CeleryTasksPeer.php
new file mode 100644
index 000000000..b9f5c7781
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/CeleryTasksPeer.php
@@ -0,0 +1,18 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for performing query and update operations on the 'celery_tasks' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.airtime
+ */
+class CeleryTasksPeer extends BaseCeleryTasksPeer
+{
+}
diff --git a/airtime_mvc/application/models/airtime/CeleryTasksQuery.php b/airtime_mvc/application/models/airtime/CeleryTasksQuery.php
new file mode 100644
index 000000000..9af7d8809
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/CeleryTasksQuery.php
@@ -0,0 +1,18 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for performing query and update operations on the 'celery_tasks' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.airtime
+ */
+class CeleryTasksQuery extends BaseCeleryTasksQuery
+{
+}
diff --git a/airtime_mvc/application/models/airtime/map/CcFilesTableMap.php b/airtime_mvc/application/models/airtime/map/CcFilesTableMap.php
index 4614577eb..5e616627f 100644
--- a/airtime_mvc/application/models/airtime/map/CcFilesTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CcFilesTableMap.php
@@ -127,6 +127,7 @@ class CcFilesTableMap extends TableMap
         $this->addRelation('CcBlockcontents', 'CcBlockcontents', RelationMap::ONE_TO_MANY, array('id' => 'file_id', ), 'CASCADE', null, 'CcBlockcontentss');
         $this->addRelation('CcSchedule', 'CcSchedule', RelationMap::ONE_TO_MANY, array('id' => 'file_id', ), 'CASCADE', null, 'CcSchedules');
         $this->addRelation('CcPlayoutHistory', 'CcPlayoutHistory', RelationMap::ONE_TO_MANY, array('id' => 'file_id', ), 'CASCADE', null, 'CcPlayoutHistorys');
+        $this->addRelation('ThirdPartyTrackReferences', 'ThirdPartyTrackReferences', RelationMap::ONE_TO_MANY, array('id' => 'file_id', ), 'CASCADE', null, 'ThirdPartyTrackReferencess');
     } // buildRelations()
 
 } // CcFilesTableMap
diff --git a/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php b/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php
index 23c00c076..78be57d28 100644
--- a/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CcPlayoutHistoryTemplateTableMap.php
@@ -51,7 +51,6 @@ class CcPlayoutHistoryTemplateTableMap extends TableMap
     public function buildRelations()
     {
         $this->addRelation('CcPlayoutHistoryTemplateField', 'CcPlayoutHistoryTemplateField', RelationMap::ONE_TO_MANY, array('id' => 'template_id', ), 'CASCADE', null, 'CcPlayoutHistoryTemplateFields');
-        $this->addRelation('ThirdPartyTrackReferences', 'ThirdPartyTrackReferences', RelationMap::ONE_TO_MANY, array('id' => 'file_id', ), 'CASCADE', null, 'ThirdPartyTrackReferencess');
     } // buildRelations()
 
 } // CcPlayoutHistoryTemplateTableMap
diff --git a/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php b/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php
new file mode 100644
index 000000000..f2ed15cb6
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php
@@ -0,0 +1,57 @@
+<?php
+
+
+
+/**
+ * This class defines the structure of the 'celery_tasks' table.
+ *
+ *
+ *
+ * This map class is used by Propel to do runtime db structure discovery.
+ * For example, the createSelectSql() method checks the type of a given column used in an
+ * ORDER BY clause to know whether it needs to apply SQL to make the ORDER BY case-insensitive
+ * (i.e. if it's a text column type).
+ *
+ * @package    propel.generator.airtime.map
+ */
+class CeleryTasksTableMap extends TableMap
+{
+
+    /**
+     * The (dot-path) name of this class
+     */
+    const CLASS_NAME = 'airtime.map.CeleryTasksTableMap';
+
+    /**
+     * Initialize the table attributes, columns and validators
+     * Relations are not initialized by this method since they are lazy loaded
+     *
+     * @return void
+     * @throws PropelException
+     */
+    public function initialize()
+    {
+        // attributes
+        $this->setName('celery_tasks');
+        $this->setPhpName('CeleryTasks');
+        $this->setClassname('CeleryTasks');
+        $this->setPackage('airtime');
+        $this->setUseIdGenerator(false);
+        // columns
+        $this->addPrimaryKey('id', 'DbId', 'VARCHAR', true, 256, null);
+        $this->addForeignKey('track_reference', 'DbTrackReference', 'INTEGER', 'third_party_track_references', 'id', true, null, null);
+        $this->addColumn('name', 'DbName', 'VARCHAR', false, 256, null);
+        $this->addColumn('dispatch_time', 'DbDispatchTime', 'TIMESTAMP', false, null, null);
+        $this->addColumn('status', 'DbStatus', 'VARCHAR', true, 256, null);
+        // validators
+    } // initialize()
+
+    /**
+     * Build the RelationMap objects for this table relationships
+     */
+    public function buildRelations()
+    {
+        $this->addRelation('ThirdPartyTrackReferences', 'ThirdPartyTrackReferences', RelationMap::MANY_TO_ONE, array('track_reference' => 'id', ), 'CASCADE', null);
+    } // buildRelations()
+
+} // CeleryTasksTableMap
diff --git a/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php b/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
index 07f49e86a..98b907c19 100644
--- a/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/ThirdPartyTrackReferencesTableMap.php
@@ -42,11 +42,9 @@ class ThirdPartyTrackReferencesTableMap extends TableMap
         $this->addPrimaryKey('id', 'DbId', 'INTEGER', true, null, null);
         $this->addColumn('service', 'DbService', 'VARCHAR', true, 256, null);
         $this->addColumn('foreign_id', 'DbForeignId', 'VARCHAR', false, 256, null);
-        $this->addColumn('broker_task_id', 'DbBrokerTaskId', 'VARCHAR', false, 256, null);
-        $this->addColumn('broker_task_name', 'DbBrokerTaskName', 'VARCHAR', false, 256, null);
-        $this->addColumn('broker_task_dispatch_time', 'DbBrokerTaskDispatchTime', 'TIMESTAMP', false, null, null);
-        $this->addForeignKey('file_id', 'DbFileId', 'INTEGER', 'cc_playout_history_template', 'id', true, null, null);
-        $this->addColumn('status', 'DbStatus', 'VARCHAR', true, 256, null);
+        $this->addForeignKey('file_id', 'DbFileId', 'INTEGER', 'cc_files', 'id', true, null, null);
+        $this->addColumn('upload_time', 'DbUploadTime', 'TIMESTAMP', false, null, null);
+        $this->addColumn('status', 'DbStatus', 'VARCHAR', false, 256, null);
         // validators
     } // initialize()
 
@@ -55,7 +53,8 @@ class ThirdPartyTrackReferencesTableMap extends TableMap
      */
     public function buildRelations()
     {
-        $this->addRelation('CcPlayoutHistoryTemplate', 'CcPlayoutHistoryTemplate', RelationMap::MANY_TO_ONE, array('file_id' => 'id', ), 'CASCADE', null);
+        $this->addRelation('CcFiles', 'CcFiles', RelationMap::MANY_TO_ONE, array('file_id' => 'id', ), 'CASCADE', null);
+        $this->addRelation('CeleryTasks', 'CeleryTasks', RelationMap::ONE_TO_MANY, array('id' => 'track_reference', ), 'CASCADE', null, 'CeleryTaskss');
     } // buildRelations()
 
 } // ThirdPartyTrackReferencesTableMap
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcFiles.php b/airtime_mvc/application/models/airtime/om/BaseCcFiles.php
index ef2d942dc..faa0964c0 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcFiles.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcFiles.php
@@ -521,6 +521,12 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
     protected $collCcPlayoutHistorys;
     protected $collCcPlayoutHistorysPartial;
 
+    /**
+     * @var        PropelObjectCollection|ThirdPartyTrackReferences[] Collection to store aggregation of ThirdPartyTrackReferences objects.
+     */
+    protected $collThirdPartyTrackReferencess;
+    protected $collThirdPartyTrackReferencessPartial;
+
     /**
      * Flag to prevent endless save loop, if this object is referenced
      * by another object which falls in this transaction.
@@ -577,6 +583,12 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
      */
     protected $ccPlayoutHistorysScheduledForDeletion = null;
 
+    /**
+     * An array of objects scheduled for deletion.
+     * @var		PropelObjectCollection
+     */
+    protected $thirdPartyTrackReferencessScheduledForDeletion = null;
+
     /**
      * Applies default values to this object.
      * This method should be called from the object's constructor (or
@@ -3298,6 +3310,8 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
 
             $this->collCcPlayoutHistorys = null;
 
+            $this->collThirdPartyTrackReferencess = null;
+
         } // if (deep)
     }
 
@@ -3550,6 +3564,23 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
                 }
             }
 
+            if ($this->thirdPartyTrackReferencessScheduledForDeletion !== null) {
+                if (!$this->thirdPartyTrackReferencessScheduledForDeletion->isEmpty()) {
+                    ThirdPartyTrackReferencesQuery::create()
+                        ->filterByPrimaryKeys($this->thirdPartyTrackReferencessScheduledForDeletion->getPrimaryKeys(false))
+                        ->delete($con);
+                    $this->thirdPartyTrackReferencessScheduledForDeletion = null;
+                }
+            }
+
+            if ($this->collThirdPartyTrackReferencess !== null) {
+                foreach ($this->collThirdPartyTrackReferencess as $referrerFK) {
+                    if (!$referrerFK->isDeleted() && ($referrerFK->isNew() || $referrerFK->isModified())) {
+                        $affectedRows += $referrerFK->save($con);
+                    }
+                }
+            }
+
             $this->alreadyInSave = false;
 
         }
@@ -4187,6 +4218,14 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
                     }
                 }
 
+                if ($this->collThirdPartyTrackReferencess !== null) {
+                    foreach ($this->collThirdPartyTrackReferencess as $referrerFK) {
+                        if (!$referrerFK->validate($columns)) {
+                            $failureMap = array_merge($failureMap, $referrerFK->getValidationFailures());
+                        }
+                    }
+                }
+
 
             $this->alreadyInValidation = false;
         }
@@ -4569,6 +4608,9 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
             if (null !== $this->collCcPlayoutHistorys) {
                 $result['CcPlayoutHistorys'] = $this->collCcPlayoutHistorys->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
             }
+            if (null !== $this->collThirdPartyTrackReferencess) {
+                $result['ThirdPartyTrackReferencess'] = $this->collThirdPartyTrackReferencess->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
+            }
         }
 
         return $result;
@@ -5170,6 +5212,12 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
                 }
             }
 
+            foreach ($this->getThirdPartyTrackReferencess() as $relObj) {
+                if ($relObj !== $this) {  // ensure that we don't try to copy a reference to ourselves
+                    $copyObj->addThirdPartyTrackReferences($relObj->copy($deepCopy));
+                }
+            }
+
             //unflag object copy
             $this->startCopy = false;
         } // if ($deepCopy)
@@ -5405,6 +5453,9 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
         if ('CcPlayoutHistory' == $relationName) {
             $this->initCcPlayoutHistorys();
         }
+        if ('ThirdPartyTrackReferences' == $relationName) {
+            $this->initThirdPartyTrackReferencess();
+        }
     }
 
     /**
@@ -6957,6 +7008,231 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
         return $this->getCcPlayoutHistorys($query, $con);
     }
 
+    /**
+     * Clears out the collThirdPartyTrackReferencess collection
+     *
+     * This does not modify the database; however, it will remove any associated objects, causing
+     * them to be refetched by subsequent calls to accessor method.
+     *
+     * @return CcFiles The current object (for fluent API support)
+     * @see        addThirdPartyTrackReferencess()
+     */
+    public function clearThirdPartyTrackReferencess()
+    {
+        $this->collThirdPartyTrackReferencess = null; // important to set this to null since that means it is uninitialized
+        $this->collThirdPartyTrackReferencessPartial = null;
+
+        return $this;
+    }
+
+    /**
+     * reset is the collThirdPartyTrackReferencess collection loaded partially
+     *
+     * @return void
+     */
+    public function resetPartialThirdPartyTrackReferencess($v = true)
+    {
+        $this->collThirdPartyTrackReferencessPartial = $v;
+    }
+
+    /**
+     * Initializes the collThirdPartyTrackReferencess collection.
+     *
+     * By default this just sets the collThirdPartyTrackReferencess collection to an empty array (like clearcollThirdPartyTrackReferencess());
+     * however, you may wish to override this method in your stub class to provide setting appropriate
+     * to your application -- for example, setting the initial array to the values stored in database.
+     *
+     * @param boolean $overrideExisting If set to true, the method call initializes
+     *                                        the collection even if it is not empty
+     *
+     * @return void
+     */
+    public function initThirdPartyTrackReferencess($overrideExisting = true)
+    {
+        if (null !== $this->collThirdPartyTrackReferencess && !$overrideExisting) {
+            return;
+        }
+        $this->collThirdPartyTrackReferencess = new PropelObjectCollection();
+        $this->collThirdPartyTrackReferencess->setModel('ThirdPartyTrackReferences');
+    }
+
+    /**
+     * Gets an array of ThirdPartyTrackReferences objects which contain a foreign key that references this object.
+     *
+     * If the $criteria is not null, it is used to always fetch the results from the database.
+     * Otherwise the results are fetched from the database the first time, then cached.
+     * Next time the same method is called without $criteria, the cached collection is returned.
+     * If this CcFiles is new, it will return
+     * an empty collection or the current collection; the criteria is ignored on a new object.
+     *
+     * @param Criteria $criteria optional Criteria object to narrow the query
+     * @param PropelPDO $con optional connection object
+     * @return PropelObjectCollection|ThirdPartyTrackReferences[] List of ThirdPartyTrackReferences objects
+     * @throws PropelException
+     */
+    public function getThirdPartyTrackReferencess($criteria = null, PropelPDO $con = null)
+    {
+        $partial = $this->collThirdPartyTrackReferencessPartial && !$this->isNew();
+        if (null === $this->collThirdPartyTrackReferencess || null !== $criteria  || $partial) {
+            if ($this->isNew() && null === $this->collThirdPartyTrackReferencess) {
+                // return empty collection
+                $this->initThirdPartyTrackReferencess();
+            } else {
+                $collThirdPartyTrackReferencess = ThirdPartyTrackReferencesQuery::create(null, $criteria)
+                    ->filterByCcFiles($this)
+                    ->find($con);
+                if (null !== $criteria) {
+                    if (false !== $this->collThirdPartyTrackReferencessPartial && count($collThirdPartyTrackReferencess)) {
+                      $this->initThirdPartyTrackReferencess(false);
+
+                      foreach ($collThirdPartyTrackReferencess as $obj) {
+                        if (false == $this->collThirdPartyTrackReferencess->contains($obj)) {
+                          $this->collThirdPartyTrackReferencess->append($obj);
+                        }
+                      }
+
+                      $this->collThirdPartyTrackReferencessPartial = true;
+                    }
+
+                    $collThirdPartyTrackReferencess->getInternalIterator()->rewind();
+
+                    return $collThirdPartyTrackReferencess;
+                }
+
+                if ($partial && $this->collThirdPartyTrackReferencess) {
+                    foreach ($this->collThirdPartyTrackReferencess as $obj) {
+                        if ($obj->isNew()) {
+                            $collThirdPartyTrackReferencess[] = $obj;
+                        }
+                    }
+                }
+
+                $this->collThirdPartyTrackReferencess = $collThirdPartyTrackReferencess;
+                $this->collThirdPartyTrackReferencessPartial = false;
+            }
+        }
+
+        return $this->collThirdPartyTrackReferencess;
+    }
+
+    /**
+     * Sets a collection of ThirdPartyTrackReferences objects related by a one-to-many relationship
+     * to the current object.
+     * It will also schedule objects for deletion based on a diff between old objects (aka persisted)
+     * and new objects from the given Propel collection.
+     *
+     * @param PropelCollection $thirdPartyTrackReferencess A Propel collection.
+     * @param PropelPDO $con Optional connection object
+     * @return CcFiles The current object (for fluent API support)
+     */
+    public function setThirdPartyTrackReferencess(PropelCollection $thirdPartyTrackReferencess, PropelPDO $con = null)
+    {
+        $thirdPartyTrackReferencessToDelete = $this->getThirdPartyTrackReferencess(new Criteria(), $con)->diff($thirdPartyTrackReferencess);
+
+
+        $this->thirdPartyTrackReferencessScheduledForDeletion = $thirdPartyTrackReferencessToDelete;
+
+        foreach ($thirdPartyTrackReferencessToDelete as $thirdPartyTrackReferencesRemoved) {
+            $thirdPartyTrackReferencesRemoved->setCcFiles(null);
+        }
+
+        $this->collThirdPartyTrackReferencess = null;
+        foreach ($thirdPartyTrackReferencess as $thirdPartyTrackReferences) {
+            $this->addThirdPartyTrackReferences($thirdPartyTrackReferences);
+        }
+
+        $this->collThirdPartyTrackReferencess = $thirdPartyTrackReferencess;
+        $this->collThirdPartyTrackReferencessPartial = false;
+
+        return $this;
+    }
+
+    /**
+     * Returns the number of related ThirdPartyTrackReferences objects.
+     *
+     * @param Criteria $criteria
+     * @param boolean $distinct
+     * @param PropelPDO $con
+     * @return int             Count of related ThirdPartyTrackReferences objects.
+     * @throws PropelException
+     */
+    public function countThirdPartyTrackReferencess(Criteria $criteria = null, $distinct = false, PropelPDO $con = null)
+    {
+        $partial = $this->collThirdPartyTrackReferencessPartial && !$this->isNew();
+        if (null === $this->collThirdPartyTrackReferencess || null !== $criteria || $partial) {
+            if ($this->isNew() && null === $this->collThirdPartyTrackReferencess) {
+                return 0;
+            }
+
+            if ($partial && !$criteria) {
+                return count($this->getThirdPartyTrackReferencess());
+            }
+            $query = ThirdPartyTrackReferencesQuery::create(null, $criteria);
+            if ($distinct) {
+                $query->distinct();
+            }
+
+            return $query
+                ->filterByCcFiles($this)
+                ->count($con);
+        }
+
+        return count($this->collThirdPartyTrackReferencess);
+    }
+
+    /**
+     * Method called to associate a ThirdPartyTrackReferences object to this object
+     * through the ThirdPartyTrackReferences foreign key attribute.
+     *
+     * @param    ThirdPartyTrackReferences $l ThirdPartyTrackReferences
+     * @return CcFiles The current object (for fluent API support)
+     */
+    public function addThirdPartyTrackReferences(ThirdPartyTrackReferences $l)
+    {
+        if ($this->collThirdPartyTrackReferencess === null) {
+            $this->initThirdPartyTrackReferencess();
+            $this->collThirdPartyTrackReferencessPartial = true;
+        }
+
+        if (!in_array($l, $this->collThirdPartyTrackReferencess->getArrayCopy(), true)) { // only add it if the **same** object is not already associated
+            $this->doAddThirdPartyTrackReferences($l);
+
+            if ($this->thirdPartyTrackReferencessScheduledForDeletion and $this->thirdPartyTrackReferencessScheduledForDeletion->contains($l)) {
+                $this->thirdPartyTrackReferencessScheduledForDeletion->remove($this->thirdPartyTrackReferencessScheduledForDeletion->search($l));
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param	ThirdPartyTrackReferences $thirdPartyTrackReferences The thirdPartyTrackReferences object to add.
+     */
+    protected function doAddThirdPartyTrackReferences($thirdPartyTrackReferences)
+    {
+        $this->collThirdPartyTrackReferencess[]= $thirdPartyTrackReferences;
+        $thirdPartyTrackReferences->setCcFiles($this);
+    }
+
+    /**
+     * @param	ThirdPartyTrackReferences $thirdPartyTrackReferences The thirdPartyTrackReferences object to remove.
+     * @return CcFiles The current object (for fluent API support)
+     */
+    public function removeThirdPartyTrackReferences($thirdPartyTrackReferences)
+    {
+        if ($this->getThirdPartyTrackReferencess()->contains($thirdPartyTrackReferences)) {
+            $this->collThirdPartyTrackReferencess->remove($this->collThirdPartyTrackReferencess->search($thirdPartyTrackReferences));
+            if (null === $this->thirdPartyTrackReferencessScheduledForDeletion) {
+                $this->thirdPartyTrackReferencessScheduledForDeletion = clone $this->collThirdPartyTrackReferencess;
+                $this->thirdPartyTrackReferencessScheduledForDeletion->clear();
+            }
+            $this->thirdPartyTrackReferencessScheduledForDeletion[]= clone $thirdPartyTrackReferences;
+            $thirdPartyTrackReferences->setCcFiles(null);
+        }
+
+        return $this;
+    }
+
     /**
      * Clears the current object and sets all attributes to their default values
      */
@@ -7086,6 +7362,11 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
                     $o->clearAllReferences($deep);
                 }
             }
+            if ($this->collThirdPartyTrackReferencess) {
+                foreach ($this->collThirdPartyTrackReferencess as $o) {
+                    $o->clearAllReferences($deep);
+                }
+            }
             if ($this->aFkOwner instanceof Persistent) {
               $this->aFkOwner->clearAllReferences($deep);
             }
@@ -7123,6 +7404,10 @@ abstract class BaseCcFiles extends BaseObject implements Persistent
             $this->collCcPlayoutHistorys->clearIterator();
         }
         $this->collCcPlayoutHistorys = null;
+        if ($this->collThirdPartyTrackReferencess instanceof PropelCollection) {
+            $this->collThirdPartyTrackReferencess->clearIterator();
+        }
+        $this->collThirdPartyTrackReferencess = null;
         $this->aFkOwner = null;
         $this->aCcSubjsRelatedByDbEditedby = null;
         $this->aCcMusicDirs = null;
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcFilesPeer.php b/airtime_mvc/application/models/airtime/om/BaseCcFilesPeer.php
index c6928a0d2..cbe44f498 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcFilesPeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcFilesPeer.php
@@ -723,6 +723,9 @@ abstract class BaseCcFilesPeer
         // Invalidate objects in CcPlayoutHistoryPeer instance pool,
         // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
         CcPlayoutHistoryPeer::clearInstancePool();
+        // Invalidate objects in ThirdPartyTrackReferencesPeer instance pool,
+        // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
+        ThirdPartyTrackReferencesPeer::clearInstancePool();
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcFilesQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcFilesQuery.php
index 01357b305..1ed4e004f 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcFilesQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcFilesQuery.php
@@ -190,6 +190,10 @@
  * @method CcFilesQuery rightJoinCcPlayoutHistory($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CcPlayoutHistory relation
  * @method CcFilesQuery innerJoinCcPlayoutHistory($relationAlias = null) Adds a INNER JOIN clause to the query using the CcPlayoutHistory relation
  *
+ * @method CcFilesQuery leftJoinThirdPartyTrackReferences($relationAlias = null) Adds a LEFT JOIN clause to the query using the ThirdPartyTrackReferences relation
+ * @method CcFilesQuery rightJoinThirdPartyTrackReferences($relationAlias = null) Adds a RIGHT JOIN clause to the query using the ThirdPartyTrackReferences relation
+ * @method CcFilesQuery innerJoinThirdPartyTrackReferences($relationAlias = null) Adds a INNER JOIN clause to the query using the ThirdPartyTrackReferences relation
+ *
  * @method CcFiles findOne(PropelPDO $con = null) Return the first CcFiles matching the query
  * @method CcFiles findOneOrCreate(PropelPDO $con = null) Return the first CcFiles matching the query, or a new CcFiles object populated from the query conditions when no match is found
  *
@@ -3509,6 +3513,80 @@ abstract class BaseCcFilesQuery extends ModelCriteria
             ->useQuery($relationAlias ? $relationAlias : 'CcPlayoutHistory', 'CcPlayoutHistoryQuery');
     }
 
+    /**
+     * Filter the query by a related ThirdPartyTrackReferences object
+     *
+     * @param   ThirdPartyTrackReferences|PropelObjectCollection $thirdPartyTrackReferences  the related object to use as filter
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return                 CcFilesQuery The current query, for fluid interface
+     * @throws PropelException - if the provided filter is invalid.
+     */
+    public function filterByThirdPartyTrackReferences($thirdPartyTrackReferences, $comparison = null)
+    {
+        if ($thirdPartyTrackReferences instanceof ThirdPartyTrackReferences) {
+            return $this
+                ->addUsingAlias(CcFilesPeer::ID, $thirdPartyTrackReferences->getDbFileId(), $comparison);
+        } elseif ($thirdPartyTrackReferences instanceof PropelObjectCollection) {
+            return $this
+                ->useThirdPartyTrackReferencesQuery()
+                ->filterByPrimaryKeys($thirdPartyTrackReferences->getPrimaryKeys())
+                ->endUse();
+        } else {
+            throw new PropelException('filterByThirdPartyTrackReferences() only accepts arguments of type ThirdPartyTrackReferences or PropelCollection');
+        }
+    }
+
+    /**
+     * Adds a JOIN clause to the query using the ThirdPartyTrackReferences relation
+     *
+     * @param     string $relationAlias optional alias for the relation
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return CcFilesQuery The current query, for fluid interface
+     */
+    public function joinThirdPartyTrackReferences($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        $tableMap = $this->getTableMap();
+        $relationMap = $tableMap->getRelation('ThirdPartyTrackReferences');
+
+        // create a ModelJoin object for this join
+        $join = new ModelJoin();
+        $join->setJoinType($joinType);
+        $join->setRelationMap($relationMap, $this->useAliasInSQL ? $this->getModelAlias() : null, $relationAlias);
+        if ($previousJoin = $this->getPreviousJoin()) {
+            $join->setPreviousJoin($previousJoin);
+        }
+
+        // add the ModelJoin to the current object
+        if ($relationAlias) {
+            $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
+            $this->addJoinObject($join, $relationAlias);
+        } else {
+            $this->addJoinObject($join, 'ThirdPartyTrackReferences');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Use the ThirdPartyTrackReferences relation ThirdPartyTrackReferences object
+     *
+     * @see       useQuery()
+     *
+     * @param     string $relationAlias optional alias for the relation,
+     *                                   to be used as main alias in the secondary query
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return   ThirdPartyTrackReferencesQuery A secondary query class using the current class as primary query
+     */
+    public function useThirdPartyTrackReferencesQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        return $this
+            ->joinThirdPartyTrackReferences($relationAlias, $joinType)
+            ->useQuery($relationAlias ? $relationAlias : 'ThirdPartyTrackReferences', 'ThirdPartyTrackReferencesQuery');
+    }
+
     /**
      * Exclude object from result
      *
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php
index a78619c7c..6a2b9230f 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplate.php
@@ -53,12 +53,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
     protected $collCcPlayoutHistoryTemplateFields;
     protected $collCcPlayoutHistoryTemplateFieldsPartial;
 
-    /**
-     * @var        PropelObjectCollection|ThirdPartyTrackReferences[] Collection to store aggregation of ThirdPartyTrackReferences objects.
-     */
-    protected $collThirdPartyTrackReferencess;
-    protected $collThirdPartyTrackReferencessPartial;
-
     /**
      * Flag to prevent endless save loop, if this object is referenced
      * by another object which falls in this transaction.
@@ -85,12 +79,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
      */
     protected $ccPlayoutHistoryTemplateFieldsScheduledForDeletion = null;
 
-    /**
-     * An array of objects scheduled for deletion.
-     * @var		PropelObjectCollection
-     */
-    protected $thirdPartyTrackReferencessScheduledForDeletion = null;
-
     /**
      * Get the [id] column value.
      *
@@ -295,8 +283,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
 
             $this->collCcPlayoutHistoryTemplateFields = null;
 
-            $this->collThirdPartyTrackReferencess = null;
-
         } // if (deep)
     }
 
@@ -438,23 +424,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                 }
             }
 
-            if ($this->thirdPartyTrackReferencessScheduledForDeletion !== null) {
-                if (!$this->thirdPartyTrackReferencessScheduledForDeletion->isEmpty()) {
-                    ThirdPartyTrackReferencesQuery::create()
-                        ->filterByPrimaryKeys($this->thirdPartyTrackReferencessScheduledForDeletion->getPrimaryKeys(false))
-                        ->delete($con);
-                    $this->thirdPartyTrackReferencessScheduledForDeletion = null;
-                }
-            }
-
-            if ($this->collThirdPartyTrackReferencess !== null) {
-                foreach ($this->collThirdPartyTrackReferencess as $referrerFK) {
-                    if (!$referrerFK->isDeleted() && ($referrerFK->isNew() || $referrerFK->isModified())) {
-                        $affectedRows += $referrerFK->save($con);
-                    }
-                }
-            }
-
             $this->alreadyInSave = false;
 
         }
@@ -620,14 +589,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                     }
                 }
 
-                if ($this->collThirdPartyTrackReferencess !== null) {
-                    foreach ($this->collThirdPartyTrackReferencess as $referrerFK) {
-                        if (!$referrerFK->validate($columns)) {
-                            $failureMap = array_merge($failureMap, $referrerFK->getValidationFailures());
-                        }
-                    }
-                }
-
 
             $this->alreadyInValidation = false;
         }
@@ -714,9 +675,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
             if (null !== $this->collCcPlayoutHistoryTemplateFields) {
                 $result['CcPlayoutHistoryTemplateFields'] = $this->collCcPlayoutHistoryTemplateFields->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
             }
-            if (null !== $this->collThirdPartyTrackReferencess) {
-                $result['ThirdPartyTrackReferencess'] = $this->collThirdPartyTrackReferencess->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
-            }
         }
 
         return $result;
@@ -880,12 +838,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                 }
             }
 
-            foreach ($this->getThirdPartyTrackReferencess() as $relObj) {
-                if ($relObj !== $this) {  // ensure that we don't try to copy a reference to ourselves
-                    $copyObj->addThirdPartyTrackReferences($relObj->copy($deepCopy));
-                }
-            }
-
             //unflag object copy
             $this->startCopy = false;
         } // if ($deepCopy)
@@ -950,9 +902,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
         if ('CcPlayoutHistoryTemplateField' == $relationName) {
             $this->initCcPlayoutHistoryTemplateFields();
         }
-        if ('ThirdPartyTrackReferences' == $relationName) {
-            $this->initThirdPartyTrackReferencess();
-        }
     }
 
     /**
@@ -1180,231 +1129,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
         return $this;
     }
 
-    /**
-     * Clears out the collThirdPartyTrackReferencess collection
-     *
-     * This does not modify the database; however, it will remove any associated objects, causing
-     * them to be refetched by subsequent calls to accessor method.
-     *
-     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
-     * @see        addThirdPartyTrackReferencess()
-     */
-    public function clearThirdPartyTrackReferencess()
-    {
-        $this->collThirdPartyTrackReferencess = null; // important to set this to null since that means it is uninitialized
-        $this->collThirdPartyTrackReferencessPartial = null;
-
-        return $this;
-    }
-
-    /**
-     * reset is the collThirdPartyTrackReferencess collection loaded partially
-     *
-     * @return void
-     */
-    public function resetPartialThirdPartyTrackReferencess($v = true)
-    {
-        $this->collThirdPartyTrackReferencessPartial = $v;
-    }
-
-    /**
-     * Initializes the collThirdPartyTrackReferencess collection.
-     *
-     * By default this just sets the collThirdPartyTrackReferencess collection to an empty array (like clearcollThirdPartyTrackReferencess());
-     * however, you may wish to override this method in your stub class to provide setting appropriate
-     * to your application -- for example, setting the initial array to the values stored in database.
-     *
-     * @param boolean $overrideExisting If set to true, the method call initializes
-     *                                        the collection even if it is not empty
-     *
-     * @return void
-     */
-    public function initThirdPartyTrackReferencess($overrideExisting = true)
-    {
-        if (null !== $this->collThirdPartyTrackReferencess && !$overrideExisting) {
-            return;
-        }
-        $this->collThirdPartyTrackReferencess = new PropelObjectCollection();
-        $this->collThirdPartyTrackReferencess->setModel('ThirdPartyTrackReferences');
-    }
-
-    /**
-     * Gets an array of ThirdPartyTrackReferences objects which contain a foreign key that references this object.
-     *
-     * If the $criteria is not null, it is used to always fetch the results from the database.
-     * Otherwise the results are fetched from the database the first time, then cached.
-     * Next time the same method is called without $criteria, the cached collection is returned.
-     * If this CcPlayoutHistoryTemplate is new, it will return
-     * an empty collection or the current collection; the criteria is ignored on a new object.
-     *
-     * @param Criteria $criteria optional Criteria object to narrow the query
-     * @param PropelPDO $con optional connection object
-     * @return PropelObjectCollection|ThirdPartyTrackReferences[] List of ThirdPartyTrackReferences objects
-     * @throws PropelException
-     */
-    public function getThirdPartyTrackReferencess($criteria = null, PropelPDO $con = null)
-    {
-        $partial = $this->collThirdPartyTrackReferencessPartial && !$this->isNew();
-        if (null === $this->collThirdPartyTrackReferencess || null !== $criteria  || $partial) {
-            if ($this->isNew() && null === $this->collThirdPartyTrackReferencess) {
-                // return empty collection
-                $this->initThirdPartyTrackReferencess();
-            } else {
-                $collThirdPartyTrackReferencess = ThirdPartyTrackReferencesQuery::create(null, $criteria)
-                    ->filterByCcPlayoutHistoryTemplate($this)
-                    ->find($con);
-                if (null !== $criteria) {
-                    if (false !== $this->collThirdPartyTrackReferencessPartial && count($collThirdPartyTrackReferencess)) {
-                      $this->initThirdPartyTrackReferencess(false);
-
-                      foreach ($collThirdPartyTrackReferencess as $obj) {
-                        if (false == $this->collThirdPartyTrackReferencess->contains($obj)) {
-                          $this->collThirdPartyTrackReferencess->append($obj);
-                        }
-                      }
-
-                      $this->collThirdPartyTrackReferencessPartial = true;
-                    }
-
-                    $collThirdPartyTrackReferencess->getInternalIterator()->rewind();
-
-                    return $collThirdPartyTrackReferencess;
-                }
-
-                if ($partial && $this->collThirdPartyTrackReferencess) {
-                    foreach ($this->collThirdPartyTrackReferencess as $obj) {
-                        if ($obj->isNew()) {
-                            $collThirdPartyTrackReferencess[] = $obj;
-                        }
-                    }
-                }
-
-                $this->collThirdPartyTrackReferencess = $collThirdPartyTrackReferencess;
-                $this->collThirdPartyTrackReferencessPartial = false;
-            }
-        }
-
-        return $this->collThirdPartyTrackReferencess;
-    }
-
-    /**
-     * Sets a collection of ThirdPartyTrackReferences objects related by a one-to-many relationship
-     * to the current object.
-     * It will also schedule objects for deletion based on a diff between old objects (aka persisted)
-     * and new objects from the given Propel collection.
-     *
-     * @param PropelCollection $thirdPartyTrackReferencess A Propel collection.
-     * @param PropelPDO $con Optional connection object
-     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
-     */
-    public function setThirdPartyTrackReferencess(PropelCollection $thirdPartyTrackReferencess, PropelPDO $con = null)
-    {
-        $thirdPartyTrackReferencessToDelete = $this->getThirdPartyTrackReferencess(new Criteria(), $con)->diff($thirdPartyTrackReferencess);
-
-
-        $this->thirdPartyTrackReferencessScheduledForDeletion = $thirdPartyTrackReferencessToDelete;
-
-        foreach ($thirdPartyTrackReferencessToDelete as $thirdPartyTrackReferencesRemoved) {
-            $thirdPartyTrackReferencesRemoved->setCcPlayoutHistoryTemplate(null);
-        }
-
-        $this->collThirdPartyTrackReferencess = null;
-        foreach ($thirdPartyTrackReferencess as $thirdPartyTrackReferences) {
-            $this->addThirdPartyTrackReferences($thirdPartyTrackReferences);
-        }
-
-        $this->collThirdPartyTrackReferencess = $thirdPartyTrackReferencess;
-        $this->collThirdPartyTrackReferencessPartial = false;
-
-        return $this;
-    }
-
-    /**
-     * Returns the number of related ThirdPartyTrackReferences objects.
-     *
-     * @param Criteria $criteria
-     * @param boolean $distinct
-     * @param PropelPDO $con
-     * @return int             Count of related ThirdPartyTrackReferences objects.
-     * @throws PropelException
-     */
-    public function countThirdPartyTrackReferencess(Criteria $criteria = null, $distinct = false, PropelPDO $con = null)
-    {
-        $partial = $this->collThirdPartyTrackReferencessPartial && !$this->isNew();
-        if (null === $this->collThirdPartyTrackReferencess || null !== $criteria || $partial) {
-            if ($this->isNew() && null === $this->collThirdPartyTrackReferencess) {
-                return 0;
-            }
-
-            if ($partial && !$criteria) {
-                return count($this->getThirdPartyTrackReferencess());
-            }
-            $query = ThirdPartyTrackReferencesQuery::create(null, $criteria);
-            if ($distinct) {
-                $query->distinct();
-            }
-
-            return $query
-                ->filterByCcPlayoutHistoryTemplate($this)
-                ->count($con);
-        }
-
-        return count($this->collThirdPartyTrackReferencess);
-    }
-
-    /**
-     * Method called to associate a ThirdPartyTrackReferences object to this object
-     * through the ThirdPartyTrackReferences foreign key attribute.
-     *
-     * @param    ThirdPartyTrackReferences $l ThirdPartyTrackReferences
-     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
-     */
-    public function addThirdPartyTrackReferences(ThirdPartyTrackReferences $l)
-    {
-        if ($this->collThirdPartyTrackReferencess === null) {
-            $this->initThirdPartyTrackReferencess();
-            $this->collThirdPartyTrackReferencessPartial = true;
-        }
-
-        if (!in_array($l, $this->collThirdPartyTrackReferencess->getArrayCopy(), true)) { // only add it if the **same** object is not already associated
-            $this->doAddThirdPartyTrackReferences($l);
-
-            if ($this->thirdPartyTrackReferencessScheduledForDeletion and $this->thirdPartyTrackReferencessScheduledForDeletion->contains($l)) {
-                $this->thirdPartyTrackReferencessScheduledForDeletion->remove($this->thirdPartyTrackReferencessScheduledForDeletion->search($l));
-            }
-        }
-
-        return $this;
-    }
-
-    /**
-     * @param	ThirdPartyTrackReferences $thirdPartyTrackReferences The thirdPartyTrackReferences object to add.
-     */
-    protected function doAddThirdPartyTrackReferences($thirdPartyTrackReferences)
-    {
-        $this->collThirdPartyTrackReferencess[]= $thirdPartyTrackReferences;
-        $thirdPartyTrackReferences->setCcPlayoutHistoryTemplate($this);
-    }
-
-    /**
-     * @param	ThirdPartyTrackReferences $thirdPartyTrackReferences The thirdPartyTrackReferences object to remove.
-     * @return CcPlayoutHistoryTemplate The current object (for fluent API support)
-     */
-    public function removeThirdPartyTrackReferences($thirdPartyTrackReferences)
-    {
-        if ($this->getThirdPartyTrackReferencess()->contains($thirdPartyTrackReferences)) {
-            $this->collThirdPartyTrackReferencess->remove($this->collThirdPartyTrackReferencess->search($thirdPartyTrackReferences));
-            if (null === $this->thirdPartyTrackReferencessScheduledForDeletion) {
-                $this->thirdPartyTrackReferencessScheduledForDeletion = clone $this->collThirdPartyTrackReferencess;
-                $this->thirdPartyTrackReferencessScheduledForDeletion->clear();
-            }
-            $this->thirdPartyTrackReferencessScheduledForDeletion[]= clone $thirdPartyTrackReferences;
-            $thirdPartyTrackReferences->setCcPlayoutHistoryTemplate(null);
-        }
-
-        return $this;
-    }
-
     /**
      * Clears the current object and sets all attributes to their default values
      */
@@ -1440,11 +1164,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
                     $o->clearAllReferences($deep);
                 }
             }
-            if ($this->collThirdPartyTrackReferencess) {
-                foreach ($this->collThirdPartyTrackReferencess as $o) {
-                    $o->clearAllReferences($deep);
-                }
-            }
 
             $this->alreadyInClearAllReferencesDeep = false;
         } // if ($deep)
@@ -1453,10 +1172,6 @@ abstract class BaseCcPlayoutHistoryTemplate extends BaseObject implements Persis
             $this->collCcPlayoutHistoryTemplateFields->clearIterator();
         }
         $this->collCcPlayoutHistoryTemplateFields = null;
-        if ($this->collThirdPartyTrackReferencess instanceof PropelCollection) {
-            $this->collThirdPartyTrackReferencess->clearIterator();
-        }
-        $this->collThirdPartyTrackReferencess = null;
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php
index f30c447fe..89c7cdc9d 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplatePeer.php
@@ -368,9 +368,6 @@ abstract class BaseCcPlayoutHistoryTemplatePeer
         // Invalidate objects in CcPlayoutHistoryTemplateFieldPeer instance pool,
         // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
         CcPlayoutHistoryTemplateFieldPeer::clearInstancePool();
-        // Invalidate objects in ThirdPartyTrackReferencesPeer instance pool,
-        // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
-        ThirdPartyTrackReferencesPeer::clearInstancePool();
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php
index 34ed52def..262b0ee2c 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlayoutHistoryTemplateQuery.php
@@ -22,10 +22,6 @@
  * @method CcPlayoutHistoryTemplateQuery rightJoinCcPlayoutHistoryTemplateField($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CcPlayoutHistoryTemplateField relation
  * @method CcPlayoutHistoryTemplateQuery innerJoinCcPlayoutHistoryTemplateField($relationAlias = null) Adds a INNER JOIN clause to the query using the CcPlayoutHistoryTemplateField relation
  *
- * @method CcPlayoutHistoryTemplateQuery leftJoinThirdPartyTrackReferences($relationAlias = null) Adds a LEFT JOIN clause to the query using the ThirdPartyTrackReferences relation
- * @method CcPlayoutHistoryTemplateQuery rightJoinThirdPartyTrackReferences($relationAlias = null) Adds a RIGHT JOIN clause to the query using the ThirdPartyTrackReferences relation
- * @method CcPlayoutHistoryTemplateQuery innerJoinThirdPartyTrackReferences($relationAlias = null) Adds a INNER JOIN clause to the query using the ThirdPartyTrackReferences relation
- *
  * @method CcPlayoutHistoryTemplate findOne(PropelPDO $con = null) Return the first CcPlayoutHistoryTemplate matching the query
  * @method CcPlayoutHistoryTemplate findOneOrCreate(PropelPDO $con = null) Return the first CcPlayoutHistoryTemplate matching the query, or a new CcPlayoutHistoryTemplate object populated from the query conditions when no match is found
  *
@@ -405,80 +401,6 @@ abstract class BaseCcPlayoutHistoryTemplateQuery extends ModelCriteria
             ->useQuery($relationAlias ? $relationAlias : 'CcPlayoutHistoryTemplateField', 'CcPlayoutHistoryTemplateFieldQuery');
     }
 
-    /**
-     * Filter the query by a related ThirdPartyTrackReferences object
-     *
-     * @param   ThirdPartyTrackReferences|PropelObjectCollection $thirdPartyTrackReferences  the related object to use as filter
-     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
-     *
-     * @return                 CcPlayoutHistoryTemplateQuery The current query, for fluid interface
-     * @throws PropelException - if the provided filter is invalid.
-     */
-    public function filterByThirdPartyTrackReferences($thirdPartyTrackReferences, $comparison = null)
-    {
-        if ($thirdPartyTrackReferences instanceof ThirdPartyTrackReferences) {
-            return $this
-                ->addUsingAlias(CcPlayoutHistoryTemplatePeer::ID, $thirdPartyTrackReferences->getDbFileId(), $comparison);
-        } elseif ($thirdPartyTrackReferences instanceof PropelObjectCollection) {
-            return $this
-                ->useThirdPartyTrackReferencesQuery()
-                ->filterByPrimaryKeys($thirdPartyTrackReferences->getPrimaryKeys())
-                ->endUse();
-        } else {
-            throw new PropelException('filterByThirdPartyTrackReferences() only accepts arguments of type ThirdPartyTrackReferences or PropelCollection');
-        }
-    }
-
-    /**
-     * Adds a JOIN clause to the query using the ThirdPartyTrackReferences relation
-     *
-     * @param     string $relationAlias optional alias for the relation
-     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
-     *
-     * @return CcPlayoutHistoryTemplateQuery The current query, for fluid interface
-     */
-    public function joinThirdPartyTrackReferences($relationAlias = null, $joinType = Criteria::INNER_JOIN)
-    {
-        $tableMap = $this->getTableMap();
-        $relationMap = $tableMap->getRelation('ThirdPartyTrackReferences');
-
-        // create a ModelJoin object for this join
-        $join = new ModelJoin();
-        $join->setJoinType($joinType);
-        $join->setRelationMap($relationMap, $this->useAliasInSQL ? $this->getModelAlias() : null, $relationAlias);
-        if ($previousJoin = $this->getPreviousJoin()) {
-            $join->setPreviousJoin($previousJoin);
-        }
-
-        // add the ModelJoin to the current object
-        if ($relationAlias) {
-            $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
-            $this->addJoinObject($join, $relationAlias);
-        } else {
-            $this->addJoinObject($join, 'ThirdPartyTrackReferences');
-        }
-
-        return $this;
-    }
-
-    /**
-     * Use the ThirdPartyTrackReferences relation ThirdPartyTrackReferences object
-     *
-     * @see       useQuery()
-     *
-     * @param     string $relationAlias optional alias for the relation,
-     *                                   to be used as main alias in the secondary query
-     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
-     *
-     * @return   ThirdPartyTrackReferencesQuery A secondary query class using the current class as primary query
-     */
-    public function useThirdPartyTrackReferencesQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
-    {
-        return $this
-            ->joinThirdPartyTrackReferences($relationAlias, $joinType)
-            ->useQuery($relationAlias ? $relationAlias : 'ThirdPartyTrackReferences', 'ThirdPartyTrackReferencesQuery');
-    }
-
     /**
      * Exclude object from result
      *
diff --git a/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php b/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php
new file mode 100644
index 000000000..c95a5f649
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php
@@ -0,0 +1,1119 @@
+<?php
+
+
+/**
+ * Base class that represents a row from the 'celery_tasks' table.
+ *
+ *
+ *
+ * @package    propel.generator.airtime.om
+ */
+abstract class BaseCeleryTasks extends BaseObject implements Persistent
+{
+    /**
+     * Peer class name
+     */
+    const PEER = 'CeleryTasksPeer';
+
+    /**
+     * The Peer class.
+     * Instance provides a convenient way of calling static methods on a class
+     * that calling code may not be able to identify.
+     * @var        CeleryTasksPeer
+     */
+    protected static $peer;
+
+    /**
+     * The flag var to prevent infinite loop in deep copy
+     * @var       boolean
+     */
+    protected $startCopy = false;
+
+    /**
+     * The value for the id field.
+     * @var        string
+     */
+    protected $id;
+
+    /**
+     * The value for the track_reference field.
+     * @var        int
+     */
+    protected $track_reference;
+
+    /**
+     * The value for the name field.
+     * @var        string
+     */
+    protected $name;
+
+    /**
+     * The value for the dispatch_time field.
+     * @var        string
+     */
+    protected $dispatch_time;
+
+    /**
+     * The value for the status field.
+     * @var        string
+     */
+    protected $status;
+
+    /**
+     * @var        ThirdPartyTrackReferences
+     */
+    protected $aThirdPartyTrackReferences;
+
+    /**
+     * Flag to prevent endless save loop, if this object is referenced
+     * by another object which falls in this transaction.
+     * @var        boolean
+     */
+    protected $alreadyInSave = false;
+
+    /**
+     * Flag to prevent endless validation loop, if this object is referenced
+     * by another object which falls in this transaction.
+     * @var        boolean
+     */
+    protected $alreadyInValidation = false;
+
+    /**
+     * Flag to prevent endless clearAllReferences($deep=true) loop, if this object is referenced
+     * @var        boolean
+     */
+    protected $alreadyInClearAllReferencesDeep = false;
+
+    /**
+     * Get the [id] column value.
+     *
+     * @return string
+     */
+    public function getDbId()
+    {
+
+        return $this->id;
+    }
+
+    /**
+     * Get the [track_reference] column value.
+     *
+     * @return int
+     */
+    public function getDbTrackReference()
+    {
+
+        return $this->track_reference;
+    }
+
+    /**
+     * Get the [name] column value.
+     *
+     * @return string
+     */
+    public function getDbName()
+    {
+
+        return $this->name;
+    }
+
+    /**
+     * Get the [optionally formatted] temporal [dispatch_time] column value.
+     *
+     *
+     * @param string $format The date/time format string (either date()-style or strftime()-style).
+     *				 If format is null, then the raw DateTime object will be returned.
+     * @return mixed Formatted date/time value as string or DateTime object (if format is null), null if column is null
+     * @throws PropelException - if unable to parse/validate the date/time value.
+     */
+    public function getDbDispatchTime($format = 'Y-m-d H:i:s')
+    {
+        if ($this->dispatch_time === null) {
+            return null;
+        }
+
+
+        try {
+            $dt = new DateTime($this->dispatch_time);
+        } catch (Exception $x) {
+            throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->dispatch_time, true), $x);
+        }
+
+        if ($format === null) {
+            // Because propel.useDateTimeClass is true, we return a DateTime object.
+            return $dt;
+        }
+
+        if (strpos($format, '%') !== false) {
+            return strftime($format, $dt->format('U'));
+        }
+
+        return $dt->format($format);
+
+    }
+
+    /**
+     * Get the [status] column value.
+     *
+     * @return string
+     */
+    public function getDbStatus()
+    {
+
+        return $this->status;
+    }
+
+    /**
+     * Set the value of [id] column.
+     *
+     * @param  string $v new value
+     * @return CeleryTasks The current object (for fluent API support)
+     */
+    public function setDbId($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->id !== $v) {
+            $this->id = $v;
+            $this->modifiedColumns[] = CeleryTasksPeer::ID;
+        }
+
+
+        return $this;
+    } // setDbId()
+
+    /**
+     * Set the value of [track_reference] column.
+     *
+     * @param  int $v new value
+     * @return CeleryTasks The current object (for fluent API support)
+     */
+    public function setDbTrackReference($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (int) $v;
+        }
+
+        if ($this->track_reference !== $v) {
+            $this->track_reference = $v;
+            $this->modifiedColumns[] = CeleryTasksPeer::TRACK_REFERENCE;
+        }
+
+        if ($this->aThirdPartyTrackReferences !== null && $this->aThirdPartyTrackReferences->getDbId() !== $v) {
+            $this->aThirdPartyTrackReferences = null;
+        }
+
+
+        return $this;
+    } // setDbTrackReference()
+
+    /**
+     * Set the value of [name] column.
+     *
+     * @param  string $v new value
+     * @return CeleryTasks The current object (for fluent API support)
+     */
+    public function setDbName($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->name !== $v) {
+            $this->name = $v;
+            $this->modifiedColumns[] = CeleryTasksPeer::NAME;
+        }
+
+
+        return $this;
+    } // setDbName()
+
+    /**
+     * Sets the value of [dispatch_time] column to a normalized version of the date/time value specified.
+     *
+     * @param mixed $v string, integer (timestamp), or DateTime value.
+     *               Empty strings are treated as null.
+     * @return CeleryTasks The current object (for fluent API support)
+     */
+    public function setDbDispatchTime($v)
+    {
+        $dt = PropelDateTime::newInstance($v, null, 'DateTime');
+        if ($this->dispatch_time !== null || $dt !== null) {
+            $currentDateAsString = ($this->dispatch_time !== null && $tmpDt = new DateTime($this->dispatch_time)) ? $tmpDt->format('Y-m-d H:i:s') : null;
+            $newDateAsString = $dt ? $dt->format('Y-m-d H:i:s') : null;
+            if ($currentDateAsString !== $newDateAsString) {
+                $this->dispatch_time = $newDateAsString;
+                $this->modifiedColumns[] = CeleryTasksPeer::DISPATCH_TIME;
+            }
+        } // if either are not null
+
+
+        return $this;
+    } // setDbDispatchTime()
+
+    /**
+     * Set the value of [status] column.
+     *
+     * @param  string $v new value
+     * @return CeleryTasks The current object (for fluent API support)
+     */
+    public function setDbStatus($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->status !== $v) {
+            $this->status = $v;
+            $this->modifiedColumns[] = CeleryTasksPeer::STATUS;
+        }
+
+
+        return $this;
+    } // setDbStatus()
+
+    /**
+     * Indicates whether the columns in this object are only set to default values.
+     *
+     * This method can be used in conjunction with isModified() to indicate whether an object is both
+     * modified _and_ has some values set which are non-default.
+     *
+     * @return boolean Whether the columns in this object are only been set with default values.
+     */
+    public function hasOnlyDefaultValues()
+    {
+        // otherwise, everything was equal, so return true
+        return true;
+    } // hasOnlyDefaultValues()
+
+    /**
+     * Hydrates (populates) the object variables with values from the database resultset.
+     *
+     * An offset (0-based "start column") is specified so that objects can be hydrated
+     * with a subset of the columns in the resultset rows.  This is needed, for example,
+     * for results of JOIN queries where the resultset row includes columns from two or
+     * more tables.
+     *
+     * @param array $row The row returned by PDOStatement->fetch(PDO::FETCH_NUM)
+     * @param int $startcol 0-based offset column which indicates which resultset column to start with.
+     * @param boolean $rehydrate Whether this object is being re-hydrated from the database.
+     * @return int             next starting column
+     * @throws PropelException - Any caught Exception will be rewrapped as a PropelException.
+     */
+    public function hydrate($row, $startcol = 0, $rehydrate = false)
+    {
+        try {
+
+            $this->id = ($row[$startcol + 0] !== null) ? (string) $row[$startcol + 0] : null;
+            $this->track_reference = ($row[$startcol + 1] !== null) ? (int) $row[$startcol + 1] : null;
+            $this->name = ($row[$startcol + 2] !== null) ? (string) $row[$startcol + 2] : null;
+            $this->dispatch_time = ($row[$startcol + 3] !== null) ? (string) $row[$startcol + 3] : null;
+            $this->status = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->resetModified();
+
+            $this->setNew(false);
+
+            if ($rehydrate) {
+                $this->ensureConsistency();
+            }
+            $this->postHydrate($row, $startcol, $rehydrate);
+
+            return $startcol + 5; // 5 = CeleryTasksPeer::NUM_HYDRATE_COLUMNS.
+
+        } catch (Exception $e) {
+            throw new PropelException("Error populating CeleryTasks object", $e);
+        }
+    }
+
+    /**
+     * Checks and repairs the internal consistency of the object.
+     *
+     * This method is executed after an already-instantiated object is re-hydrated
+     * from the database.  It exists to check any foreign keys to make sure that
+     * the objects related to the current object are correct based on foreign key.
+     *
+     * You can override this method in the stub class, but you should always invoke
+     * the base method from the overridden method (i.e. parent::ensureConsistency()),
+     * in case your model changes.
+     *
+     * @throws PropelException
+     */
+    public function ensureConsistency()
+    {
+
+        if ($this->aThirdPartyTrackReferences !== null && $this->track_reference !== $this->aThirdPartyTrackReferences->getDbId()) {
+            $this->aThirdPartyTrackReferences = null;
+        }
+    } // ensureConsistency
+
+    /**
+     * Reloads this object from datastore based on primary key and (optionally) resets all associated objects.
+     *
+     * This will only work if the object has been saved and has a valid primary key set.
+     *
+     * @param boolean $deep (optional) Whether to also de-associated any related objects.
+     * @param PropelPDO $con (optional) The PropelPDO connection to use.
+     * @return void
+     * @throws PropelException - if this object is deleted, unsaved or doesn't have pk match in db
+     */
+    public function reload($deep = false, PropelPDO $con = null)
+    {
+        if ($this->isDeleted()) {
+            throw new PropelException("Cannot reload a deleted object.");
+        }
+
+        if ($this->isNew()) {
+            throw new PropelException("Cannot reload an unsaved object.");
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        // We don't need to alter the object instance pool; we're just modifying this instance
+        // already in the pool.
+
+        $stmt = CeleryTasksPeer::doSelectStmt($this->buildPkeyCriteria(), $con);
+        $row = $stmt->fetch(PDO::FETCH_NUM);
+        $stmt->closeCursor();
+        if (!$row) {
+            throw new PropelException('Cannot find matching row in the database to reload object values.');
+        }
+        $this->hydrate($row, 0, true); // rehydrate
+
+        if ($deep) {  // also de-associate any related objects?
+
+            $this->aThirdPartyTrackReferences = null;
+        } // if (deep)
+    }
+
+    /**
+     * Removes this object from datastore and sets delete attribute.
+     *
+     * @param PropelPDO $con
+     * @return void
+     * @throws PropelException
+     * @throws Exception
+     * @see        BaseObject::setDeleted()
+     * @see        BaseObject::isDeleted()
+     */
+    public function delete(PropelPDO $con = null)
+    {
+        if ($this->isDeleted()) {
+            throw new PropelException("This object has already been deleted.");
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        $con->beginTransaction();
+        try {
+            $deleteQuery = CeleryTasksQuery::create()
+                ->filterByPrimaryKey($this->getPrimaryKey());
+            $ret = $this->preDelete($con);
+            if ($ret) {
+                $deleteQuery->delete($con);
+                $this->postDelete($con);
+                $con->commit();
+                $this->setDeleted(true);
+            } else {
+                $con->commit();
+            }
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Persists this object to the database.
+     *
+     * If the object is new, it inserts it; otherwise an update is performed.
+     * All modified related objects will also be persisted in the doSave()
+     * method.  This method wraps all precipitate database operations in a
+     * single transaction.
+     *
+     * @param PropelPDO $con
+     * @return int             The number of rows affected by this insert/update and any referring fk objects' save() operations.
+     * @throws PropelException
+     * @throws Exception
+     * @see        doSave()
+     */
+    public function save(PropelPDO $con = null)
+    {
+        if ($this->isDeleted()) {
+            throw new PropelException("You cannot save an object that has been deleted.");
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        $con->beginTransaction();
+        $isInsert = $this->isNew();
+        try {
+            $ret = $this->preSave($con);
+            if ($isInsert) {
+                $ret = $ret && $this->preInsert($con);
+            } else {
+                $ret = $ret && $this->preUpdate($con);
+            }
+            if ($ret) {
+                $affectedRows = $this->doSave($con);
+                if ($isInsert) {
+                    $this->postInsert($con);
+                } else {
+                    $this->postUpdate($con);
+                }
+                $this->postSave($con);
+                CeleryTasksPeer::addInstanceToPool($this);
+            } else {
+                $affectedRows = 0;
+            }
+            $con->commit();
+
+            return $affectedRows;
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Performs the work of inserting or updating the row in the database.
+     *
+     * If the object is new, it inserts it; otherwise an update is performed.
+     * All related objects are also updated in this method.
+     *
+     * @param PropelPDO $con
+     * @return int             The number of rows affected by this insert/update and any referring fk objects' save() operations.
+     * @throws PropelException
+     * @see        save()
+     */
+    protected function doSave(PropelPDO $con)
+    {
+        $affectedRows = 0; // initialize var to track total num of affected rows
+        if (!$this->alreadyInSave) {
+            $this->alreadyInSave = true;
+
+            // We call the save method on the following object(s) if they
+            // were passed to this object by their corresponding set
+            // method.  This object relates to these object(s) by a
+            // foreign key reference.
+
+            if ($this->aThirdPartyTrackReferences !== null) {
+                if ($this->aThirdPartyTrackReferences->isModified() || $this->aThirdPartyTrackReferences->isNew()) {
+                    $affectedRows += $this->aThirdPartyTrackReferences->save($con);
+                }
+                $this->setThirdPartyTrackReferences($this->aThirdPartyTrackReferences);
+            }
+
+            if ($this->isNew() || $this->isModified()) {
+                // persist changes
+                if ($this->isNew()) {
+                    $this->doInsert($con);
+                } else {
+                    $this->doUpdate($con);
+                }
+                $affectedRows += 1;
+                $this->resetModified();
+            }
+
+            $this->alreadyInSave = false;
+
+        }
+
+        return $affectedRows;
+    } // doSave()
+
+    /**
+     * Insert the row in the database.
+     *
+     * @param PropelPDO $con
+     *
+     * @throws PropelException
+     * @see        doSave()
+     */
+    protected function doInsert(PropelPDO $con)
+    {
+        $modifiedColumns = array();
+        $index = 0;
+
+
+         // check the columns in natural order for more readable SQL queries
+        if ($this->isColumnModified(CeleryTasksPeer::ID)) {
+            $modifiedColumns[':p' . $index++]  = '"id"';
+        }
+        if ($this->isColumnModified(CeleryTasksPeer::TRACK_REFERENCE)) {
+            $modifiedColumns[':p' . $index++]  = '"track_reference"';
+        }
+        if ($this->isColumnModified(CeleryTasksPeer::NAME)) {
+            $modifiedColumns[':p' . $index++]  = '"name"';
+        }
+        if ($this->isColumnModified(CeleryTasksPeer::DISPATCH_TIME)) {
+            $modifiedColumns[':p' . $index++]  = '"dispatch_time"';
+        }
+        if ($this->isColumnModified(CeleryTasksPeer::STATUS)) {
+            $modifiedColumns[':p' . $index++]  = '"status"';
+        }
+
+        $sql = sprintf(
+            'INSERT INTO "celery_tasks" (%s) VALUES (%s)',
+            implode(', ', $modifiedColumns),
+            implode(', ', array_keys($modifiedColumns))
+        );
+
+        try {
+            $stmt = $con->prepare($sql);
+            foreach ($modifiedColumns as $identifier => $columnName) {
+                switch ($columnName) {
+                    case '"id"':
+                        $stmt->bindValue($identifier, $this->id, PDO::PARAM_STR);
+                        break;
+                    case '"track_reference"':
+                        $stmt->bindValue($identifier, $this->track_reference, PDO::PARAM_INT);
+                        break;
+                    case '"name"':
+                        $stmt->bindValue($identifier, $this->name, PDO::PARAM_STR);
+                        break;
+                    case '"dispatch_time"':
+                        $stmt->bindValue($identifier, $this->dispatch_time, PDO::PARAM_STR);
+                        break;
+                    case '"status"':
+                        $stmt->bindValue($identifier, $this->status, PDO::PARAM_STR);
+                        break;
+                }
+            }
+            $stmt->execute();
+        } catch (Exception $e) {
+            Propel::log($e->getMessage(), Propel::LOG_ERR);
+            throw new PropelException(sprintf('Unable to execute INSERT statement [%s]', $sql), $e);
+        }
+
+        $this->setNew(false);
+    }
+
+    /**
+     * Update the row in the database.
+     *
+     * @param PropelPDO $con
+     *
+     * @see        doSave()
+     */
+    protected function doUpdate(PropelPDO $con)
+    {
+        $selectCriteria = $this->buildPkeyCriteria();
+        $valuesCriteria = $this->buildCriteria();
+        BasePeer::doUpdate($selectCriteria, $valuesCriteria, $con);
+    }
+
+    /**
+     * Array of ValidationFailed objects.
+     * @var        array ValidationFailed[]
+     */
+    protected $validationFailures = array();
+
+    /**
+     * Gets any ValidationFailed objects that resulted from last call to validate().
+     *
+     *
+     * @return array ValidationFailed[]
+     * @see        validate()
+     */
+    public function getValidationFailures()
+    {
+        return $this->validationFailures;
+    }
+
+    /**
+     * Validates the objects modified field values and all objects related to this table.
+     *
+     * If $columns is either a column name or an array of column names
+     * only those columns are validated.
+     *
+     * @param mixed $columns Column name or an array of column names.
+     * @return boolean Whether all columns pass validation.
+     * @see        doValidate()
+     * @see        getValidationFailures()
+     */
+    public function validate($columns = null)
+    {
+        $res = $this->doValidate($columns);
+        if ($res === true) {
+            $this->validationFailures = array();
+
+            return true;
+        }
+
+        $this->validationFailures = $res;
+
+        return false;
+    }
+
+    /**
+     * This function performs the validation work for complex object models.
+     *
+     * In addition to checking the current object, all related objects will
+     * also be validated.  If all pass then <code>true</code> is returned; otherwise
+     * an aggregated array of ValidationFailed objects will be returned.
+     *
+     * @param array $columns Array of column names to validate.
+     * @return mixed <code>true</code> if all validations pass; array of <code>ValidationFailed</code> objects otherwise.
+     */
+    protected function doValidate($columns = null)
+    {
+        if (!$this->alreadyInValidation) {
+            $this->alreadyInValidation = true;
+            $retval = null;
+
+            $failureMap = array();
+
+
+            // We call the validate method on the following object(s) if they
+            // were passed to this object by their corresponding set
+            // method.  This object relates to these object(s) by a
+            // foreign key reference.
+
+            if ($this->aThirdPartyTrackReferences !== null) {
+                if (!$this->aThirdPartyTrackReferences->validate($columns)) {
+                    $failureMap = array_merge($failureMap, $this->aThirdPartyTrackReferences->getValidationFailures());
+                }
+            }
+
+
+            if (($retval = CeleryTasksPeer::doValidate($this, $columns)) !== true) {
+                $failureMap = array_merge($failureMap, $retval);
+            }
+
+
+
+            $this->alreadyInValidation = false;
+        }
+
+        return (!empty($failureMap) ? $failureMap : true);
+    }
+
+    /**
+     * Retrieves a field from the object by name passed in as a string.
+     *
+     * @param string $name name
+     * @param string $type The type of fieldname the $name is of:
+     *               one of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *               BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     *               Defaults to BasePeer::TYPE_PHPNAME
+     * @return mixed Value of field.
+     */
+    public function getByName($name, $type = BasePeer::TYPE_PHPNAME)
+    {
+        $pos = CeleryTasksPeer::translateFieldName($name, $type, BasePeer::TYPE_NUM);
+        $field = $this->getByPosition($pos);
+
+        return $field;
+    }
+
+    /**
+     * Retrieves a field from the object by Position as specified in the xml schema.
+     * Zero-based.
+     *
+     * @param int $pos position in xml schema
+     * @return mixed Value of field at $pos
+     */
+    public function getByPosition($pos)
+    {
+        switch ($pos) {
+            case 0:
+                return $this->getDbId();
+                break;
+            case 1:
+                return $this->getDbTrackReference();
+                break;
+            case 2:
+                return $this->getDbName();
+                break;
+            case 3:
+                return $this->getDbDispatchTime();
+                break;
+            case 4:
+                return $this->getDbStatus();
+                break;
+            default:
+                return null;
+                break;
+        } // switch()
+    }
+
+    /**
+     * Exports the object as an array.
+     *
+     * You can specify the key type of the array by passing one of the class
+     * type constants.
+     *
+     * @param     string  $keyType (optional) One of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME,
+     *                    BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     *                    Defaults to BasePeer::TYPE_PHPNAME.
+     * @param     boolean $includeLazyLoadColumns (optional) Whether to include lazy loaded columns. Defaults to true.
+     * @param     array $alreadyDumpedObjects List of objects to skip to avoid recursion
+     * @param     boolean $includeForeignObjects (optional) Whether to include hydrated related objects. Default to FALSE.
+     *
+     * @return array an associative array containing the field names (as keys) and field values
+     */
+    public function toArray($keyType = BasePeer::TYPE_PHPNAME, $includeLazyLoadColumns = true, $alreadyDumpedObjects = array(), $includeForeignObjects = false)
+    {
+        if (isset($alreadyDumpedObjects['CeleryTasks'][$this->getPrimaryKey()])) {
+            return '*RECURSION*';
+        }
+        $alreadyDumpedObjects['CeleryTasks'][$this->getPrimaryKey()] = true;
+        $keys = CeleryTasksPeer::getFieldNames($keyType);
+        $result = array(
+            $keys[0] => $this->getDbId(),
+            $keys[1] => $this->getDbTrackReference(),
+            $keys[2] => $this->getDbName(),
+            $keys[3] => $this->getDbDispatchTime(),
+            $keys[4] => $this->getDbStatus(),
+        );
+        $virtualColumns = $this->virtualColumns;
+        foreach ($virtualColumns as $key => $virtualColumn) {
+            $result[$key] = $virtualColumn;
+        }
+
+        if ($includeForeignObjects) {
+            if (null !== $this->aThirdPartyTrackReferences) {
+                $result['ThirdPartyTrackReferences'] = $this->aThirdPartyTrackReferences->toArray($keyType, $includeLazyLoadColumns,  $alreadyDumpedObjects, true);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Sets a field from the object by name passed in as a string.
+     *
+     * @param string $name peer name
+     * @param mixed $value field value
+     * @param string $type The type of fieldname the $name is of:
+     *                     one of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *                     BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     *                     Defaults to BasePeer::TYPE_PHPNAME
+     * @return void
+     */
+    public function setByName($name, $value, $type = BasePeer::TYPE_PHPNAME)
+    {
+        $pos = CeleryTasksPeer::translateFieldName($name, $type, BasePeer::TYPE_NUM);
+
+        $this->setByPosition($pos, $value);
+    }
+
+    /**
+     * Sets a field from the object by Position as specified in the xml schema.
+     * Zero-based.
+     *
+     * @param int $pos position in xml schema
+     * @param mixed $value field value
+     * @return void
+     */
+    public function setByPosition($pos, $value)
+    {
+        switch ($pos) {
+            case 0:
+                $this->setDbId($value);
+                break;
+            case 1:
+                $this->setDbTrackReference($value);
+                break;
+            case 2:
+                $this->setDbName($value);
+                break;
+            case 3:
+                $this->setDbDispatchTime($value);
+                break;
+            case 4:
+                $this->setDbStatus($value);
+                break;
+        } // switch()
+    }
+
+    /**
+     * Populates the object using an array.
+     *
+     * This is particularly useful when populating an object from one of the
+     * request arrays (e.g. $_POST).  This method goes through the column
+     * names, checking to see whether a matching key exists in populated
+     * array. If so the setByName() method is called for that column.
+     *
+     * You can specify the key type of the array by additionally passing one
+     * of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME,
+     * BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM.
+     * The default key type is the column's BasePeer::TYPE_PHPNAME
+     *
+     * @param array  $arr     An array to populate the object from.
+     * @param string $keyType The type of keys the array uses.
+     * @return void
+     */
+    public function fromArray($arr, $keyType = BasePeer::TYPE_PHPNAME)
+    {
+        $keys = CeleryTasksPeer::getFieldNames($keyType);
+
+        if (array_key_exists($keys[0], $arr)) $this->setDbId($arr[$keys[0]]);
+        if (array_key_exists($keys[1], $arr)) $this->setDbTrackReference($arr[$keys[1]]);
+        if (array_key_exists($keys[2], $arr)) $this->setDbName($arr[$keys[2]]);
+        if (array_key_exists($keys[3], $arr)) $this->setDbDispatchTime($arr[$keys[3]]);
+        if (array_key_exists($keys[4], $arr)) $this->setDbStatus($arr[$keys[4]]);
+    }
+
+    /**
+     * Build a Criteria object containing the values of all modified columns in this object.
+     *
+     * @return Criteria The Criteria object containing all modified values.
+     */
+    public function buildCriteria()
+    {
+        $criteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
+
+        if ($this->isColumnModified(CeleryTasksPeer::ID)) $criteria->add(CeleryTasksPeer::ID, $this->id);
+        if ($this->isColumnModified(CeleryTasksPeer::TRACK_REFERENCE)) $criteria->add(CeleryTasksPeer::TRACK_REFERENCE, $this->track_reference);
+        if ($this->isColumnModified(CeleryTasksPeer::NAME)) $criteria->add(CeleryTasksPeer::NAME, $this->name);
+        if ($this->isColumnModified(CeleryTasksPeer::DISPATCH_TIME)) $criteria->add(CeleryTasksPeer::DISPATCH_TIME, $this->dispatch_time);
+        if ($this->isColumnModified(CeleryTasksPeer::STATUS)) $criteria->add(CeleryTasksPeer::STATUS, $this->status);
+
+        return $criteria;
+    }
+
+    /**
+     * Builds a Criteria object containing the primary key for this object.
+     *
+     * Unlike buildCriteria() this method includes the primary key values regardless
+     * of whether or not they have been modified.
+     *
+     * @return Criteria The Criteria object containing value(s) for primary key(s).
+     */
+    public function buildPkeyCriteria()
+    {
+        $criteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
+        $criteria->add(CeleryTasksPeer::ID, $this->id);
+
+        return $criteria;
+    }
+
+    /**
+     * Returns the primary key for this object (row).
+     * @return string
+     */
+    public function getPrimaryKey()
+    {
+        return $this->getDbId();
+    }
+
+    /**
+     * Generic method to set the primary key (id column).
+     *
+     * @param  string $key Primary key.
+     * @return void
+     */
+    public function setPrimaryKey($key)
+    {
+        $this->setDbId($key);
+    }
+
+    /**
+     * Returns true if the primary key for this object is null.
+     * @return boolean
+     */
+    public function isPrimaryKeyNull()
+    {
+
+        return null === $this->getDbId();
+    }
+
+    /**
+     * Sets contents of passed object to values from current object.
+     *
+     * If desired, this method can also make copies of all associated (fkey referrers)
+     * objects.
+     *
+     * @param object $copyObj An object of CeleryTasks (or compatible) type.
+     * @param boolean $deepCopy Whether to also copy all rows that refer (by fkey) to the current row.
+     * @param boolean $makeNew Whether to reset autoincrement PKs and make the object new.
+     * @throws PropelException
+     */
+    public function copyInto($copyObj, $deepCopy = false, $makeNew = true)
+    {
+        $copyObj->setDbTrackReference($this->getDbTrackReference());
+        $copyObj->setDbName($this->getDbName());
+        $copyObj->setDbDispatchTime($this->getDbDispatchTime());
+        $copyObj->setDbStatus($this->getDbStatus());
+
+        if ($deepCopy && !$this->startCopy) {
+            // important: temporarily setNew(false) because this affects the behavior of
+            // the getter/setter methods for fkey referrer objects.
+            $copyObj->setNew(false);
+            // store object hash to prevent cycle
+            $this->startCopy = true;
+
+            //unflag object copy
+            $this->startCopy = false;
+        } // if ($deepCopy)
+
+        if ($makeNew) {
+            $copyObj->setNew(true);
+            $copyObj->setDbId(NULL); // this is a auto-increment column, so set to default value
+        }
+    }
+
+    /**
+     * Makes a copy of this object that will be inserted as a new row in table when saved.
+     * It creates a new object filling in the simple attributes, but skipping any primary
+     * keys that are defined for the table.
+     *
+     * If desired, this method can also make copies of all associated (fkey referrers)
+     * objects.
+     *
+     * @param boolean $deepCopy Whether to also copy all rows that refer (by fkey) to the current row.
+     * @return CeleryTasks Clone of current object.
+     * @throws PropelException
+     */
+    public function copy($deepCopy = false)
+    {
+        // we use get_class(), because this might be a subclass
+        $clazz = get_class($this);
+        $copyObj = new $clazz();
+        $this->copyInto($copyObj, $deepCopy);
+
+        return $copyObj;
+    }
+
+    /**
+     * Returns a peer instance associated with this om.
+     *
+     * Since Peer classes are not to have any instance attributes, this method returns the
+     * same instance for all member of this class. The method could therefore
+     * be static, but this would prevent one from overriding the behavior.
+     *
+     * @return CeleryTasksPeer
+     */
+    public function getPeer()
+    {
+        if (self::$peer === null) {
+            self::$peer = new CeleryTasksPeer();
+        }
+
+        return self::$peer;
+    }
+
+    /**
+     * Declares an association between this object and a ThirdPartyTrackReferences object.
+     *
+     * @param                  ThirdPartyTrackReferences $v
+     * @return CeleryTasks The current object (for fluent API support)
+     * @throws PropelException
+     */
+    public function setThirdPartyTrackReferences(ThirdPartyTrackReferences $v = null)
+    {
+        if ($v === null) {
+            $this->setDbTrackReference(NULL);
+        } else {
+            $this->setDbTrackReference($v->getDbId());
+        }
+
+        $this->aThirdPartyTrackReferences = $v;
+
+        // Add binding for other direction of this n:n relationship.
+        // If this object has already been added to the ThirdPartyTrackReferences object, it will not be re-added.
+        if ($v !== null) {
+            $v->addCeleryTasks($this);
+        }
+
+
+        return $this;
+    }
+
+
+    /**
+     * Get the associated ThirdPartyTrackReferences object
+     *
+     * @param PropelPDO $con Optional Connection object.
+     * @param $doQuery Executes a query to get the object if required
+     * @return ThirdPartyTrackReferences The associated ThirdPartyTrackReferences object.
+     * @throws PropelException
+     */
+    public function getThirdPartyTrackReferences(PropelPDO $con = null, $doQuery = true)
+    {
+        if ($this->aThirdPartyTrackReferences === null && ($this->track_reference !== null) && $doQuery) {
+            $this->aThirdPartyTrackReferences = ThirdPartyTrackReferencesQuery::create()->findPk($this->track_reference, $con);
+            /* The following can be used additionally to
+                guarantee the related object contains a reference
+                to this object.  This level of coupling may, however, be
+                undesirable since it could result in an only partially populated collection
+                in the referenced object.
+                $this->aThirdPartyTrackReferences->addCeleryTaskss($this);
+             */
+        }
+
+        return $this->aThirdPartyTrackReferences;
+    }
+
+    /**
+     * Clears the current object and sets all attributes to their default values
+     */
+    public function clear()
+    {
+        $this->id = null;
+        $this->track_reference = null;
+        $this->name = null;
+        $this->dispatch_time = null;
+        $this->status = null;
+        $this->alreadyInSave = false;
+        $this->alreadyInValidation = false;
+        $this->alreadyInClearAllReferencesDeep = false;
+        $this->clearAllReferences();
+        $this->resetModified();
+        $this->setNew(true);
+        $this->setDeleted(false);
+    }
+
+    /**
+     * Resets all references to other model objects or collections of model objects.
+     *
+     * This method is a user-space workaround for PHP's inability to garbage collect
+     * objects with circular references (even in PHP 5.3). This is currently necessary
+     * when using Propel in certain daemon or large-volume/high-memory operations.
+     *
+     * @param boolean $deep Whether to also clear the references on all referrer objects.
+     */
+    public function clearAllReferences($deep = false)
+    {
+        if ($deep && !$this->alreadyInClearAllReferencesDeep) {
+            $this->alreadyInClearAllReferencesDeep = true;
+            if ($this->aThirdPartyTrackReferences instanceof Persistent) {
+              $this->aThirdPartyTrackReferences->clearAllReferences($deep);
+            }
+
+            $this->alreadyInClearAllReferencesDeep = false;
+        } // if ($deep)
+
+        $this->aThirdPartyTrackReferences = null;
+    }
+
+    /**
+     * return the string representation of this object
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return (string) $this->exportTo(CeleryTasksPeer::DEFAULT_STRING_FORMAT);
+    }
+
+    /**
+     * return true is the object is in saving state
+     *
+     * @return boolean
+     */
+    public function isAlreadyInSave()
+    {
+        return $this->alreadyInSave;
+    }
+
+}
diff --git a/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php
new file mode 100644
index 000000000..2a651fd0e
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php
@@ -0,0 +1,1010 @@
+<?php
+
+
+/**
+ * Base static class for performing query and update operations on the 'celery_tasks' table.
+ *
+ *
+ *
+ * @package propel.generator.airtime.om
+ */
+abstract class BaseCeleryTasksPeer
+{
+
+    /** the default database name for this class */
+    const DATABASE_NAME = 'airtime';
+
+    /** the table name for this class */
+    const TABLE_NAME = 'celery_tasks';
+
+    /** the related Propel class for this table */
+    const OM_CLASS = 'CeleryTasks';
+
+    /** the related TableMap class for this table */
+    const TM_CLASS = 'CeleryTasksTableMap';
+
+    /** The total number of columns. */
+    const NUM_COLUMNS = 5;
+
+    /** The number of lazy-loaded columns. */
+    const NUM_LAZY_LOAD_COLUMNS = 0;
+
+    /** The number of columns to hydrate (NUM_COLUMNS - NUM_LAZY_LOAD_COLUMNS) */
+    const NUM_HYDRATE_COLUMNS = 5;
+
+    /** the column name for the id field */
+    const ID = 'celery_tasks.id';
+
+    /** the column name for the track_reference field */
+    const TRACK_REFERENCE = 'celery_tasks.track_reference';
+
+    /** the column name for the name field */
+    const NAME = 'celery_tasks.name';
+
+    /** the column name for the dispatch_time field */
+    const DISPATCH_TIME = 'celery_tasks.dispatch_time';
+
+    /** the column name for the status field */
+    const STATUS = 'celery_tasks.status';
+
+    /** The default string format for model objects of the related table **/
+    const DEFAULT_STRING_FORMAT = 'YAML';
+
+    /**
+     * An identity map to hold any loaded instances of CeleryTasks objects.
+     * This must be public so that other peer classes can access this when hydrating from JOIN
+     * queries.
+     * @var        array CeleryTasks[]
+     */
+    public static $instances = array();
+
+
+    /**
+     * holds an array of fieldnames
+     *
+     * first dimension keys are the type constants
+     * e.g. CeleryTasksPeer::$fieldNames[CeleryTasksPeer::TYPE_PHPNAME][0] = 'Id'
+     */
+    protected static $fieldNames = array (
+        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbTrackReference', 'DbName', 'DbDispatchTime', 'DbStatus', ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbTrackReference', 'dbName', 'dbDispatchTime', 'dbStatus', ),
+        BasePeer::TYPE_COLNAME => array (CeleryTasksPeer::ID, CeleryTasksPeer::TRACK_REFERENCE, CeleryTasksPeer::NAME, CeleryTasksPeer::DISPATCH_TIME, CeleryTasksPeer::STATUS, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'TRACK_REFERENCE', 'NAME', 'DISPATCH_TIME', 'STATUS', ),
+        BasePeer::TYPE_FIELDNAME => array ('id', 'track_reference', 'name', 'dispatch_time', 'status', ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+    );
+
+    /**
+     * holds an array of keys for quick access to the fieldnames array
+     *
+     * first dimension keys are the type constants
+     * e.g. CeleryTasksPeer::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0
+     */
+    protected static $fieldKeys = array (
+        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbTrackReference' => 1, 'DbName' => 2, 'DbDispatchTime' => 3, 'DbStatus' => 4, ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbTrackReference' => 1, 'dbName' => 2, 'dbDispatchTime' => 3, 'dbStatus' => 4, ),
+        BasePeer::TYPE_COLNAME => array (CeleryTasksPeer::ID => 0, CeleryTasksPeer::TRACK_REFERENCE => 1, CeleryTasksPeer::NAME => 2, CeleryTasksPeer::DISPATCH_TIME => 3, CeleryTasksPeer::STATUS => 4, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'TRACK_REFERENCE' => 1, 'NAME' => 2, 'DISPATCH_TIME' => 3, 'STATUS' => 4, ),
+        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'track_reference' => 1, 'name' => 2, 'dispatch_time' => 3, 'status' => 4, ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+    );
+
+    /**
+     * Translates a fieldname to another type
+     *
+     * @param      string $name field name
+     * @param      string $fromType One of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *                         BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM
+     * @param      string $toType   One of the class type constants
+     * @return string          translated name of the field.
+     * @throws PropelException - if the specified name could not be found in the fieldname mappings.
+     */
+    public static function translateFieldName($name, $fromType, $toType)
+    {
+        $toNames = CeleryTasksPeer::getFieldNames($toType);
+        $key = isset(CeleryTasksPeer::$fieldKeys[$fromType][$name]) ? CeleryTasksPeer::$fieldKeys[$fromType][$name] : null;
+        if ($key === null) {
+            throw new PropelException("'$name' could not be found in the field names of type '$fromType'. These are: " . print_r(CeleryTasksPeer::$fieldKeys[$fromType], true));
+        }
+
+        return $toNames[$key];
+    }
+
+    /**
+     * Returns an array of field names.
+     *
+     * @param      string $type The type of fieldnames to return:
+     *                      One of the class type constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME
+     *                      BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM
+     * @return array           A list of field names
+     * @throws PropelException - if the type is not valid.
+     */
+    public static function getFieldNames($type = BasePeer::TYPE_PHPNAME)
+    {
+        if (!array_key_exists($type, CeleryTasksPeer::$fieldNames)) {
+            throw new PropelException('Method getFieldNames() expects the parameter $type to be one of the class constants BasePeer::TYPE_PHPNAME, BasePeer::TYPE_STUDLYPHPNAME, BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_NUM. ' . $type . ' was given.');
+        }
+
+        return CeleryTasksPeer::$fieldNames[$type];
+    }
+
+    /**
+     * Convenience method which changes table.column to alias.column.
+     *
+     * Using this method you can maintain SQL abstraction while using column aliases.
+     * <code>
+     *		$c->addAlias("alias1", TablePeer::TABLE_NAME);
+     *		$c->addJoin(TablePeer::alias("alias1", TablePeer::PRIMARY_KEY_COLUMN), TablePeer::PRIMARY_KEY_COLUMN);
+     * </code>
+     * @param      string $alias The alias for the current table.
+     * @param      string $column The column name for current table. (i.e. CeleryTasksPeer::COLUMN_NAME).
+     * @return string
+     */
+    public static function alias($alias, $column)
+    {
+        return str_replace(CeleryTasksPeer::TABLE_NAME.'.', $alias.'.', $column);
+    }
+
+    /**
+     * Add all the columns needed to create a new object.
+     *
+     * Note: any columns that were marked with lazyLoad="true" in the
+     * XML schema will not be added to the select list and only loaded
+     * on demand.
+     *
+     * @param      Criteria $criteria object containing the columns to add.
+     * @param      string   $alias    optional table alias
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function addSelectColumns(Criteria $criteria, $alias = null)
+    {
+        if (null === $alias) {
+            $criteria->addSelectColumn(CeleryTasksPeer::ID);
+            $criteria->addSelectColumn(CeleryTasksPeer::TRACK_REFERENCE);
+            $criteria->addSelectColumn(CeleryTasksPeer::NAME);
+            $criteria->addSelectColumn(CeleryTasksPeer::DISPATCH_TIME);
+            $criteria->addSelectColumn(CeleryTasksPeer::STATUS);
+        } else {
+            $criteria->addSelectColumn($alias . '.id');
+            $criteria->addSelectColumn($alias . '.track_reference');
+            $criteria->addSelectColumn($alias . '.name');
+            $criteria->addSelectColumn($alias . '.dispatch_time');
+            $criteria->addSelectColumn($alias . '.status');
+        }
+    }
+
+    /**
+     * Returns the number of rows matching criteria.
+     *
+     * @param      Criteria $criteria
+     * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
+     * @param      PropelPDO $con
+     * @return int Number of matching rows.
+     */
+    public static function doCount(Criteria $criteria, $distinct = false, PropelPDO $con = null)
+    {
+        // we may modify criteria, so copy it first
+        $criteria = clone $criteria;
+
+        // We need to set the primary table name, since in the case that there are no WHERE columns
+        // it will be impossible for the BasePeer::createSelectSql() method to determine which
+        // tables go into the FROM clause.
+        $criteria->setPrimaryTableName(CeleryTasksPeer::TABLE_NAME);
+
+        if ($distinct && !in_array(Criteria::DISTINCT, $criteria->getSelectModifiers())) {
+            $criteria->setDistinct();
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            CeleryTasksPeer::addSelectColumns($criteria);
+        }
+
+        $criteria->clearOrderByColumns(); // ORDER BY won't ever affect the count
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME); // Set the correct dbName
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+        // BasePeer returns a PDOStatement
+        $stmt = BasePeer::doCount($criteria, $con);
+
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $count = (int) $row[0];
+        } else {
+            $count = 0; // no rows returned; we infer that means 0 matches.
+        }
+        $stmt->closeCursor();
+
+        return $count;
+    }
+    /**
+     * Selects one object from the DB.
+     *
+     * @param      Criteria $criteria object used to create the SELECT statement.
+     * @param      PropelPDO $con
+     * @return CeleryTasks
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelectOne(Criteria $criteria, PropelPDO $con = null)
+    {
+        $critcopy = clone $criteria;
+        $critcopy->setLimit(1);
+        $objects = CeleryTasksPeer::doSelect($critcopy, $con);
+        if ($objects) {
+            return $objects[0];
+        }
+
+        return null;
+    }
+    /**
+     * Selects several row from the DB.
+     *
+     * @param      Criteria $criteria The Criteria object used to build the SELECT statement.
+     * @param      PropelPDO $con
+     * @return array           Array of selected Objects
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelect(Criteria $criteria, PropelPDO $con = null)
+    {
+        return CeleryTasksPeer::populateObjects(CeleryTasksPeer::doSelectStmt($criteria, $con));
+    }
+    /**
+     * Prepares the Criteria object and uses the parent doSelect() method to execute a PDOStatement.
+     *
+     * Use this method directly if you want to work with an executed statement directly (for example
+     * to perform your own object hydration).
+     *
+     * @param      Criteria $criteria The Criteria object used to build the SELECT statement.
+     * @param      PropelPDO $con The connection to use
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     * @return PDOStatement The executed PDOStatement object.
+     * @see        BasePeer::doSelect()
+     */
+    public static function doSelectStmt(Criteria $criteria, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            $criteria = clone $criteria;
+            CeleryTasksPeer::addSelectColumns($criteria);
+        }
+
+        // Set the correct dbName
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+
+        // BasePeer returns a PDOStatement
+        return BasePeer::doSelect($criteria, $con);
+    }
+    /**
+     * Adds an object to the instance pool.
+     *
+     * Propel keeps cached copies of objects in an instance pool when they are retrieved
+     * from the database.  In some cases -- especially when you override doSelect*()
+     * methods in your stub classes -- you may need to explicitly add objects
+     * to the cache in order to ensure that the same objects are always returned by doSelect*()
+     * and retrieveByPK*() calls.
+     *
+     * @param CeleryTasks $obj A CeleryTasks object.
+     * @param      string $key (optional) key to use for instance map (for performance boost if key was already calculated externally).
+     */
+    public static function addInstanceToPool($obj, $key = null)
+    {
+        if (Propel::isInstancePoolingEnabled()) {
+            if ($key === null) {
+                $key = (string) $obj->getDbId();
+            } // if key === null
+            CeleryTasksPeer::$instances[$key] = $obj;
+        }
+    }
+
+    /**
+     * Removes an object from the instance pool.
+     *
+     * Propel keeps cached copies of objects in an instance pool when they are retrieved
+     * from the database.  In some cases -- especially when you override doDelete
+     * methods in your stub classes -- you may need to explicitly remove objects
+     * from the cache in order to prevent returning objects that no longer exist.
+     *
+     * @param      mixed $value A CeleryTasks object or a primary key value.
+     *
+     * @return void
+     * @throws PropelException - if the value is invalid.
+     */
+    public static function removeInstanceFromPool($value)
+    {
+        if (Propel::isInstancePoolingEnabled() && $value !== null) {
+            if (is_object($value) && $value instanceof CeleryTasks) {
+                $key = (string) $value->getDbId();
+            } elseif (is_scalar($value)) {
+                // assume we've been passed a primary key
+                $key = (string) $value;
+            } else {
+                $e = new PropelException("Invalid value passed to removeInstanceFromPool().  Expected primary key or CeleryTasks object; got " . (is_object($value) ? get_class($value) . ' object.' : var_export($value,true)));
+                throw $e;
+            }
+
+            unset(CeleryTasksPeer::$instances[$key]);
+        }
+    } // removeInstanceFromPool()
+
+    /**
+     * Retrieves a string version of the primary key from the DB resultset row that can be used to uniquely identify a row in this table.
+     *
+     * For tables with a single-column primary key, that simple pkey value will be returned.  For tables with
+     * a multi-column primary key, a serialize()d version of the primary key will be returned.
+     *
+     * @param      string $key The key (@see getPrimaryKeyHash()) for this instance.
+     * @return CeleryTasks Found object or null if 1) no instance exists for specified key or 2) instance pooling has been disabled.
+     * @see        getPrimaryKeyHash()
+     */
+    public static function getInstanceFromPool($key)
+    {
+        if (Propel::isInstancePoolingEnabled()) {
+            if (isset(CeleryTasksPeer::$instances[$key])) {
+                return CeleryTasksPeer::$instances[$key];
+            }
+        }
+
+        return null; // just to be explicit
+    }
+
+    /**
+     * Clear the instance pool.
+     *
+     * @return void
+     */
+    public static function clearInstancePool($and_clear_all_references = false)
+    {
+      if ($and_clear_all_references) {
+        foreach (CeleryTasksPeer::$instances as $instance) {
+          $instance->clearAllReferences(true);
+        }
+      }
+        CeleryTasksPeer::$instances = array();
+    }
+
+    /**
+     * Method to invalidate the instance pool of all tables related to celery_tasks
+     * by a foreign key with ON DELETE CASCADE
+     */
+    public static function clearRelatedInstancePool()
+    {
+    }
+
+    /**
+     * Retrieves a string version of the primary key from the DB resultset row that can be used to uniquely identify a row in this table.
+     *
+     * For tables with a single-column primary key, that simple pkey value will be returned.  For tables with
+     * a multi-column primary key, a serialize()d version of the primary key will be returned.
+     *
+     * @param      array $row PropelPDO resultset row.
+     * @param      int $startcol The 0-based offset for reading from the resultset row.
+     * @return string A string version of PK or null if the components of primary key in result array are all null.
+     */
+    public static function getPrimaryKeyHashFromRow($row, $startcol = 0)
+    {
+        // If the PK cannot be derived from the row, return null.
+        if ($row[$startcol] === null) {
+            return null;
+        }
+
+        return (string) $row[$startcol];
+    }
+
+    /**
+     * Retrieves the primary key from the DB resultset row
+     * For tables with a single-column primary key, that simple pkey value will be returned.  For tables with
+     * a multi-column primary key, an array of the primary key columns will be returned.
+     *
+     * @param      array $row PropelPDO resultset row.
+     * @param      int $startcol The 0-based offset for reading from the resultset row.
+     * @return mixed The primary key of the row
+     */
+    public static function getPrimaryKeyFromRow($row, $startcol = 0)
+    {
+
+        return (string) $row[$startcol];
+    }
+
+    /**
+     * The returned array will contain objects of the default type or
+     * objects that inherit from the default.
+     *
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function populateObjects(PDOStatement $stmt)
+    {
+        $results = array();
+
+        // set the class once to avoid overhead in the loop
+        $cls = CeleryTasksPeer::getOMClass();
+        // populate the object(s)
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $key = CeleryTasksPeer::getPrimaryKeyHashFromRow($row, 0);
+            if (null !== ($obj = CeleryTasksPeer::getInstanceFromPool($key))) {
+                // We no longer rehydrate the object, since this can cause data loss.
+                // See http://www.propelorm.org/ticket/509
+                // $obj->hydrate($row, 0, true); // rehydrate
+                $results[] = $obj;
+            } else {
+                $obj = new $cls();
+                $obj->hydrate($row);
+                $results[] = $obj;
+                CeleryTasksPeer::addInstanceToPool($obj, $key);
+            } // if key exists
+        }
+        $stmt->closeCursor();
+
+        return $results;
+    }
+    /**
+     * Populates an object of the default type or an object that inherit from the default.
+     *
+     * @param      array $row PropelPDO resultset row.
+     * @param      int $startcol The 0-based offset for reading from the resultset row.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     * @return array (CeleryTasks object, last column rank)
+     */
+    public static function populateObject($row, $startcol = 0)
+    {
+        $key = CeleryTasksPeer::getPrimaryKeyHashFromRow($row, $startcol);
+        if (null !== ($obj = CeleryTasksPeer::getInstanceFromPool($key))) {
+            // We no longer rehydrate the object, since this can cause data loss.
+            // See http://www.propelorm.org/ticket/509
+            // $obj->hydrate($row, $startcol, true); // rehydrate
+            $col = $startcol + CeleryTasksPeer::NUM_HYDRATE_COLUMNS;
+        } else {
+            $cls = CeleryTasksPeer::OM_CLASS;
+            $obj = new $cls();
+            $col = $obj->hydrate($row, $startcol);
+            CeleryTasksPeer::addInstanceToPool($obj, $key);
+        }
+
+        return array($obj, $col);
+    }
+
+
+    /**
+     * Returns the number of rows matching criteria, joining the related ThirdPartyTrackReferences table
+     *
+     * @param      Criteria $criteria
+     * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return int Number of matching rows.
+     */
+    public static function doCountJoinThirdPartyTrackReferences(Criteria $criteria, $distinct = false, PropelPDO $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        // we're going to modify criteria, so copy it first
+        $criteria = clone $criteria;
+
+        // We need to set the primary table name, since in the case that there are no WHERE columns
+        // it will be impossible for the BasePeer::createSelectSql() method to determine which
+        // tables go into the FROM clause.
+        $criteria->setPrimaryTableName(CeleryTasksPeer::TABLE_NAME);
+
+        if ($distinct && !in_array(Criteria::DISTINCT, $criteria->getSelectModifiers())) {
+            $criteria->setDistinct();
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            CeleryTasksPeer::addSelectColumns($criteria);
+        }
+
+        $criteria->clearOrderByColumns(); // ORDER BY won't ever affect the count
+
+        // Set the correct dbName
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $criteria->addJoin(CeleryTasksPeer::TRACK_REFERENCE, ThirdPartyTrackReferencesPeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doCount($criteria, $con);
+
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $count = (int) $row[0];
+        } else {
+            $count = 0; // no rows returned; we infer that means 0 matches.
+        }
+        $stmt->closeCursor();
+
+        return $count;
+    }
+
+
+    /**
+     * Selects a collection of CeleryTasks objects pre-filled with their ThirdPartyTrackReferences objects.
+     * @param      Criteria  $criteria
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return array           Array of CeleryTasks objects.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelectJoinThirdPartyTrackReferences(Criteria $criteria, $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        $criteria = clone $criteria;
+
+        // Set the correct dbName if it has not been overridden
+        if ($criteria->getDbName() == Propel::getDefaultDB()) {
+            $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+        }
+
+        CeleryTasksPeer::addSelectColumns($criteria);
+        $startcol = CeleryTasksPeer::NUM_HYDRATE_COLUMNS;
+        ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+
+        $criteria->addJoin(CeleryTasksPeer::TRACK_REFERENCE, ThirdPartyTrackReferencesPeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doSelect($criteria, $con);
+        $results = array();
+
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $key1 = CeleryTasksPeer::getPrimaryKeyHashFromRow($row, 0);
+            if (null !== ($obj1 = CeleryTasksPeer::getInstanceFromPool($key1))) {
+                // We no longer rehydrate the object, since this can cause data loss.
+                // See http://www.propelorm.org/ticket/509
+                // $obj1->hydrate($row, 0, true); // rehydrate
+            } else {
+
+                $cls = CeleryTasksPeer::getOMClass();
+
+                $obj1 = new $cls();
+                $obj1->hydrate($row);
+                CeleryTasksPeer::addInstanceToPool($obj1, $key1);
+            } // if $obj1 already loaded
+
+            $key2 = ThirdPartyTrackReferencesPeer::getPrimaryKeyHashFromRow($row, $startcol);
+            if ($key2 !== null) {
+                $obj2 = ThirdPartyTrackReferencesPeer::getInstanceFromPool($key2);
+                if (!$obj2) {
+
+                    $cls = ThirdPartyTrackReferencesPeer::getOMClass();
+
+                    $obj2 = new $cls();
+                    $obj2->hydrate($row, $startcol);
+                    ThirdPartyTrackReferencesPeer::addInstanceToPool($obj2, $key2);
+                } // if obj2 already loaded
+
+                // Add the $obj1 (CeleryTasks) to $obj2 (ThirdPartyTrackReferences)
+                $obj2->addCeleryTasks($obj1);
+
+            } // if joined row was not null
+
+            $results[] = $obj1;
+        }
+        $stmt->closeCursor();
+
+        return $results;
+    }
+
+
+    /**
+     * Returns the number of rows matching criteria, joining all related tables
+     *
+     * @param      Criteria $criteria
+     * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return int Number of matching rows.
+     */
+    public static function doCountJoinAll(Criteria $criteria, $distinct = false, PropelPDO $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        // we're going to modify criteria, so copy it first
+        $criteria = clone $criteria;
+
+        // We need to set the primary table name, since in the case that there are no WHERE columns
+        // it will be impossible for the BasePeer::createSelectSql() method to determine which
+        // tables go into the FROM clause.
+        $criteria->setPrimaryTableName(CeleryTasksPeer::TABLE_NAME);
+
+        if ($distinct && !in_array(Criteria::DISTINCT, $criteria->getSelectModifiers())) {
+            $criteria->setDistinct();
+        }
+
+        if (!$criteria->hasSelectClause()) {
+            CeleryTasksPeer::addSelectColumns($criteria);
+        }
+
+        $criteria->clearOrderByColumns(); // ORDER BY won't ever affect the count
+
+        // Set the correct dbName
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $criteria->addJoin(CeleryTasksPeer::TRACK_REFERENCE, ThirdPartyTrackReferencesPeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doCount($criteria, $con);
+
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $count = (int) $row[0];
+        } else {
+            $count = 0; // no rows returned; we infer that means 0 matches.
+        }
+        $stmt->closeCursor();
+
+        return $count;
+    }
+
+    /**
+     * Selects a collection of CeleryTasks objects pre-filled with all related objects.
+     *
+     * @param      Criteria  $criteria
+     * @param      PropelPDO $con
+     * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
+     * @return array           Array of CeleryTasks objects.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doSelectJoinAll(Criteria $criteria, $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    {
+        $criteria = clone $criteria;
+
+        // Set the correct dbName if it has not been overridden
+        if ($criteria->getDbName() == Propel::getDefaultDB()) {
+            $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+        }
+
+        CeleryTasksPeer::addSelectColumns($criteria);
+        $startcol2 = CeleryTasksPeer::NUM_HYDRATE_COLUMNS;
+
+        ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
+        $startcol3 = $startcol2 + ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS;
+
+        $criteria->addJoin(CeleryTasksPeer::TRACK_REFERENCE, ThirdPartyTrackReferencesPeer::ID, $join_behavior);
+
+        $stmt = BasePeer::doSelect($criteria, $con);
+        $results = array();
+
+        while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $key1 = CeleryTasksPeer::getPrimaryKeyHashFromRow($row, 0);
+            if (null !== ($obj1 = CeleryTasksPeer::getInstanceFromPool($key1))) {
+                // We no longer rehydrate the object, since this can cause data loss.
+                // See http://www.propelorm.org/ticket/509
+                // $obj1->hydrate($row, 0, true); // rehydrate
+            } else {
+                $cls = CeleryTasksPeer::getOMClass();
+
+                $obj1 = new $cls();
+                $obj1->hydrate($row);
+                CeleryTasksPeer::addInstanceToPool($obj1, $key1);
+            } // if obj1 already loaded
+
+            // Add objects for joined ThirdPartyTrackReferences rows
+
+            $key2 = ThirdPartyTrackReferencesPeer::getPrimaryKeyHashFromRow($row, $startcol2);
+            if ($key2 !== null) {
+                $obj2 = ThirdPartyTrackReferencesPeer::getInstanceFromPool($key2);
+                if (!$obj2) {
+
+                    $cls = ThirdPartyTrackReferencesPeer::getOMClass();
+
+                    $obj2 = new $cls();
+                    $obj2->hydrate($row, $startcol2);
+                    ThirdPartyTrackReferencesPeer::addInstanceToPool($obj2, $key2);
+                } // if obj2 loaded
+
+                // Add the $obj1 (CeleryTasks) to the collection in $obj2 (ThirdPartyTrackReferences)
+                $obj2->addCeleryTasks($obj1);
+            } // if joined row not null
+
+            $results[] = $obj1;
+        }
+        $stmt->closeCursor();
+
+        return $results;
+    }
+
+    /**
+     * Returns the TableMap related to this peer.
+     * This method is not needed for general use but a specific application could have a need.
+     * @return TableMap
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function getTableMap()
+    {
+        return Propel::getDatabaseMap(CeleryTasksPeer::DATABASE_NAME)->getTable(CeleryTasksPeer::TABLE_NAME);
+    }
+
+    /**
+     * Add a TableMap instance to the database for this peer class.
+     */
+    public static function buildTableMap()
+    {
+      $dbMap = Propel::getDatabaseMap(BaseCeleryTasksPeer::DATABASE_NAME);
+      if (!$dbMap->hasTable(BaseCeleryTasksPeer::TABLE_NAME)) {
+        $dbMap->addTableObject(new \CeleryTasksTableMap());
+      }
+    }
+
+    /**
+     * The class that the Peer will make instances of.
+     *
+     *
+     * @return string ClassName
+     */
+    public static function getOMClass($row = 0, $colnum = 0)
+    {
+        return CeleryTasksPeer::OM_CLASS;
+    }
+
+    /**
+     * Performs an INSERT on the database, given a CeleryTasks or Criteria object.
+     *
+     * @param      mixed $values Criteria or CeleryTasks object containing data that is used to create the INSERT statement.
+     * @param      PropelPDO $con the PropelPDO connection to use
+     * @return mixed           The new primary key.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doInsert($values, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        if ($values instanceof Criteria) {
+            $criteria = clone $values; // rename for clarity
+        } else {
+            $criteria = $values->buildCriteria(); // build Criteria from CeleryTasks object
+        }
+
+
+        // Set the correct dbName
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+
+        try {
+            // use transaction because $criteria could contain info
+            // for more than one table (I guess, conceivably)
+            $con->beginTransaction();
+            $pk = BasePeer::doInsert($criteria, $con);
+            $con->commit();
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+
+        return $pk;
+    }
+
+    /**
+     * Performs an UPDATE on the database, given a CeleryTasks or Criteria object.
+     *
+     * @param      mixed $values Criteria or CeleryTasks object containing data that is used to create the UPDATE statement.
+     * @param      PropelPDO $con The connection to use (specify PropelPDO connection object to exert more control over transactions).
+     * @return int             The number of affected rows (if supported by underlying database driver).
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function doUpdate($values, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        $selectCriteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
+
+        if ($values instanceof Criteria) {
+            $criteria = clone $values; // rename for clarity
+
+            $comparison = $criteria->getComparison(CeleryTasksPeer::ID);
+            $value = $criteria->remove(CeleryTasksPeer::ID);
+            if ($value) {
+                $selectCriteria->add(CeleryTasksPeer::ID, $value, $comparison);
+            } else {
+                $selectCriteria->setPrimaryTableName(CeleryTasksPeer::TABLE_NAME);
+            }
+
+        } else { // $values is CeleryTasks object
+            $criteria = $values->buildCriteria(); // gets full criteria
+            $selectCriteria = $values->buildPkeyCriteria(); // gets criteria w/ primary key(s)
+        }
+
+        // set the correct dbName
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+
+        return BasePeer::doUpdate($selectCriteria, $criteria, $con);
+    }
+
+    /**
+     * Deletes all rows from the celery_tasks table.
+     *
+     * @param      PropelPDO $con the connection to use
+     * @return int             The number of affected rows (if supported by underlying database driver).
+     * @throws PropelException
+     */
+    public static function doDeleteAll(PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+        $affectedRows = 0; // initialize var to track total num of affected rows
+        try {
+            // use transaction because $criteria could contain info
+            // for more than one table or we could emulating ON DELETE CASCADE, etc.
+            $con->beginTransaction();
+            $affectedRows += BasePeer::doDeleteAll(CeleryTasksPeer::TABLE_NAME, $con, CeleryTasksPeer::DATABASE_NAME);
+            // Because this db requires some delete cascade/set null emulation, we have to
+            // clear the cached instance *after* the emulation has happened (since
+            // instances get re-added by the select statement contained therein).
+            CeleryTasksPeer::clearInstancePool();
+            CeleryTasksPeer::clearRelatedInstancePool();
+            $con->commit();
+
+            return $affectedRows;
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Performs a DELETE on the database, given a CeleryTasks or Criteria object OR a primary key value.
+     *
+     * @param      mixed $values Criteria or CeleryTasks object or primary key or array of primary keys
+     *              which is used to create the DELETE statement
+     * @param      PropelPDO $con the connection to use
+     * @return int The number of affected rows (if supported by underlying database driver).  This includes CASCADE-related rows
+     *				if supported by native driver or if emulated using Propel.
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+     public static function doDelete($values, PropelPDO $con = null)
+     {
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
+        }
+
+        if ($values instanceof Criteria) {
+            // invalidate the cache for all objects of this type, since we have no
+            // way of knowing (without running a query) what objects should be invalidated
+            // from the cache based on this Criteria.
+            CeleryTasksPeer::clearInstancePool();
+            // rename for clarity
+            $criteria = clone $values;
+        } elseif ($values instanceof CeleryTasks) { // it's a model object
+            // invalidate the cache for this single object
+            CeleryTasksPeer::removeInstanceFromPool($values);
+            // create criteria based on pk values
+            $criteria = $values->buildPkeyCriteria();
+        } else { // it's a primary key, or an array of pks
+            $criteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
+            $criteria->add(CeleryTasksPeer::ID, (array) $values, Criteria::IN);
+            // invalidate the cache for this object(s)
+            foreach ((array) $values as $singleval) {
+                CeleryTasksPeer::removeInstanceFromPool($singleval);
+            }
+        }
+
+        // Set the correct dbName
+        $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
+
+        $affectedRows = 0; // initialize var to track total num of affected rows
+
+        try {
+            // use transaction because $criteria could contain info
+            // for more than one table or we could emulating ON DELETE CASCADE, etc.
+            $con->beginTransaction();
+
+            $affectedRows += BasePeer::doDelete($criteria, $con);
+            CeleryTasksPeer::clearRelatedInstancePool();
+            $con->commit();
+
+            return $affectedRows;
+        } catch (Exception $e) {
+            $con->rollBack();
+            throw $e;
+        }
+    }
+
+    /**
+     * Validates all modified columns of given CeleryTasks object.
+     * If parameter $columns is either a single column name or an array of column names
+     * than only those columns are validated.
+     *
+     * NOTICE: This does not apply to primary or foreign keys for now.
+     *
+     * @param CeleryTasks $obj The object to validate.
+     * @param      mixed $cols Column name or array of column names.
+     *
+     * @return mixed TRUE if all columns are valid or the error message of the first invalid column.
+     */
+    public static function doValidate($obj, $cols = null)
+    {
+        $columns = array();
+
+        if ($cols) {
+            $dbMap = Propel::getDatabaseMap(CeleryTasksPeer::DATABASE_NAME);
+            $tableMap = $dbMap->getTable(CeleryTasksPeer::TABLE_NAME);
+
+            if (! is_array($cols)) {
+                $cols = array($cols);
+            }
+
+            foreach ($cols as $colName) {
+                if ($tableMap->hasColumn($colName)) {
+                    $get = 'get' . $tableMap->getColumn($colName)->getPhpName();
+                    $columns[$colName] = $obj->$get();
+                }
+            }
+        } else {
+
+        }
+
+        return BasePeer::doValidate(CeleryTasksPeer::DATABASE_NAME, CeleryTasksPeer::TABLE_NAME, $columns);
+    }
+
+    /**
+     * Retrieve a single object by pkey.
+     *
+     * @param string $pk the primary key.
+     * @param      PropelPDO $con the connection to use
+     * @return CeleryTasks
+     */
+    public static function retrieveByPK($pk, PropelPDO $con = null)
+    {
+
+        if (null !== ($obj = CeleryTasksPeer::getInstanceFromPool((string) $pk))) {
+            return $obj;
+        }
+
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $criteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
+        $criteria->add(CeleryTasksPeer::ID, $pk);
+
+        $v = CeleryTasksPeer::doSelect($criteria, $con);
+
+        return !empty($v) > 0 ? $v[0] : null;
+    }
+
+    /**
+     * Retrieve multiple objects by pkey.
+     *
+     * @param      array $pks List of primary keys
+     * @param      PropelPDO $con the connection to use
+     * @return CeleryTasks[]
+     * @throws PropelException Any exceptions caught during processing will be
+     *		 rethrown wrapped into a PropelException.
+     */
+    public static function retrieveByPKs($pks, PropelPDO $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+
+        $objs = null;
+        if (empty($pks)) {
+            $objs = array();
+        } else {
+            $criteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
+            $criteria->add(CeleryTasksPeer::ID, $pks, Criteria::IN);
+            $objs = CeleryTasksPeer::doSelect($criteria, $con);
+        }
+
+        return $objs;
+    }
+
+} // BaseCeleryTasksPeer
+
+// This is the static code needed to register the TableMap for this table with the main Propel class.
+//
+BaseCeleryTasksPeer::buildTableMap();
+
diff --git a/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php
new file mode 100644
index 000000000..20e683d70
--- /dev/null
+++ b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php
@@ -0,0 +1,504 @@
+<?php
+
+
+/**
+ * Base class that represents a query for the 'celery_tasks' table.
+ *
+ *
+ *
+ * @method CeleryTasksQuery orderByDbId($order = Criteria::ASC) Order by the id column
+ * @method CeleryTasksQuery orderByDbTrackReference($order = Criteria::ASC) Order by the track_reference column
+ * @method CeleryTasksQuery orderByDbName($order = Criteria::ASC) Order by the name column
+ * @method CeleryTasksQuery orderByDbDispatchTime($order = Criteria::ASC) Order by the dispatch_time column
+ * @method CeleryTasksQuery orderByDbStatus($order = Criteria::ASC) Order by the status column
+ *
+ * @method CeleryTasksQuery groupByDbId() Group by the id column
+ * @method CeleryTasksQuery groupByDbTrackReference() Group by the track_reference column
+ * @method CeleryTasksQuery groupByDbName() Group by the name column
+ * @method CeleryTasksQuery groupByDbDispatchTime() Group by the dispatch_time column
+ * @method CeleryTasksQuery groupByDbStatus() Group by the status column
+ *
+ * @method CeleryTasksQuery leftJoin($relation) Adds a LEFT JOIN clause to the query
+ * @method CeleryTasksQuery rightJoin($relation) Adds a RIGHT JOIN clause to the query
+ * @method CeleryTasksQuery innerJoin($relation) Adds a INNER JOIN clause to the query
+ *
+ * @method CeleryTasksQuery leftJoinThirdPartyTrackReferences($relationAlias = null) Adds a LEFT JOIN clause to the query using the ThirdPartyTrackReferences relation
+ * @method CeleryTasksQuery rightJoinThirdPartyTrackReferences($relationAlias = null) Adds a RIGHT JOIN clause to the query using the ThirdPartyTrackReferences relation
+ * @method CeleryTasksQuery innerJoinThirdPartyTrackReferences($relationAlias = null) Adds a INNER JOIN clause to the query using the ThirdPartyTrackReferences relation
+ *
+ * @method CeleryTasks findOne(PropelPDO $con = null) Return the first CeleryTasks matching the query
+ * @method CeleryTasks findOneOrCreate(PropelPDO $con = null) Return the first CeleryTasks matching the query, or a new CeleryTasks object populated from the query conditions when no match is found
+ *
+ * @method CeleryTasks findOneByDbTrackReference(int $track_reference) Return the first CeleryTasks filtered by the track_reference column
+ * @method CeleryTasks findOneByDbName(string $name) Return the first CeleryTasks filtered by the name column
+ * @method CeleryTasks findOneByDbDispatchTime(string $dispatch_time) Return the first CeleryTasks filtered by the dispatch_time column
+ * @method CeleryTasks findOneByDbStatus(string $status) Return the first CeleryTasks filtered by the status column
+ *
+ * @method array findByDbId(string $id) Return CeleryTasks objects filtered by the id column
+ * @method array findByDbTrackReference(int $track_reference) Return CeleryTasks objects filtered by the track_reference column
+ * @method array findByDbName(string $name) Return CeleryTasks objects filtered by the name column
+ * @method array findByDbDispatchTime(string $dispatch_time) Return CeleryTasks objects filtered by the dispatch_time column
+ * @method array findByDbStatus(string $status) Return CeleryTasks objects filtered by the status column
+ *
+ * @package    propel.generator.airtime.om
+ */
+abstract class BaseCeleryTasksQuery extends ModelCriteria
+{
+    /**
+     * Initializes internal state of BaseCeleryTasksQuery object.
+     *
+     * @param     string $dbName The dabase name
+     * @param     string $modelName The phpName of a model, e.g. 'Book'
+     * @param     string $modelAlias The alias for the model in this query, e.g. 'b'
+     */
+    public function __construct($dbName = null, $modelName = null, $modelAlias = null)
+    {
+        if (null === $dbName) {
+            $dbName = 'airtime';
+        }
+        if (null === $modelName) {
+            $modelName = 'CeleryTasks';
+        }
+        parent::__construct($dbName, $modelName, $modelAlias);
+    }
+
+    /**
+     * Returns a new CeleryTasksQuery object.
+     *
+     * @param     string $modelAlias The alias of a model in the query
+     * @param   CeleryTasksQuery|Criteria $criteria Optional Criteria to build the query from
+     *
+     * @return CeleryTasksQuery
+     */
+    public static function create($modelAlias = null, $criteria = null)
+    {
+        if ($criteria instanceof CeleryTasksQuery) {
+            return $criteria;
+        }
+        $query = new CeleryTasksQuery(null, null, $modelAlias);
+
+        if ($criteria instanceof Criteria) {
+            $query->mergeWith($criteria);
+        }
+
+        return $query;
+    }
+
+    /**
+     * Find object by primary key.
+     * Propel uses the instance pool to skip the database if the object exists.
+     * Go fast if the query is untouched.
+     *
+     * <code>
+     * $obj  = $c->findPk(12, $con);
+     * </code>
+     *
+     * @param mixed $key Primary key to use for the query
+     * @param     PropelPDO $con an optional connection object
+     *
+     * @return   CeleryTasks|CeleryTasks[]|mixed the result, formatted by the current formatter
+     */
+    public function findPk($key, $con = null)
+    {
+        if ($key === null) {
+            return null;
+        }
+        if ((null !== ($obj = CeleryTasksPeer::getInstanceFromPool((string) $key))) && !$this->formatter) {
+            // the object is already in the instance pool
+            return $obj;
+        }
+        if ($con === null) {
+            $con = Propel::getConnection(CeleryTasksPeer::DATABASE_NAME, Propel::CONNECTION_READ);
+        }
+        $this->basePreSelect($con);
+        if ($this->formatter || $this->modelAlias || $this->with || $this->select
+         || $this->selectColumns || $this->asColumns || $this->selectModifiers
+         || $this->map || $this->having || $this->joins) {
+            return $this->findPkComplex($key, $con);
+        } else {
+            return $this->findPkSimple($key, $con);
+        }
+    }
+
+    /**
+     * Alias of findPk to use instance pooling
+     *
+     * @param     mixed $key Primary key to use for the query
+     * @param     PropelPDO $con A connection object
+     *
+     * @return                 CeleryTasks A model object, or null if the key is not found
+     * @throws PropelException
+     */
+     public function findOneByDbId($key, $con = null)
+     {
+        return $this->findPk($key, $con);
+     }
+
+    /**
+     * Find object by primary key using raw SQL to go fast.
+     * Bypass doSelect() and the object formatter by using generated code.
+     *
+     * @param     mixed $key Primary key to use for the query
+     * @param     PropelPDO $con A connection object
+     *
+     * @return                 CeleryTasks A model object, or null if the key is not found
+     * @throws PropelException
+     */
+    protected function findPkSimple($key, $con)
+    {
+        $sql = 'SELECT "id", "track_reference", "name", "dispatch_time", "status" FROM "celery_tasks" WHERE "id" = :p0';
+        try {
+            $stmt = $con->prepare($sql);
+            $stmt->bindValue(':p0', $key, PDO::PARAM_STR);
+            $stmt->execute();
+        } catch (Exception $e) {
+            Propel::log($e->getMessage(), Propel::LOG_ERR);
+            throw new PropelException(sprintf('Unable to execute SELECT statement [%s]', $sql), $e);
+        }
+        $obj = null;
+        if ($row = $stmt->fetch(PDO::FETCH_NUM)) {
+            $obj = new CeleryTasks();
+            $obj->hydrate($row);
+            CeleryTasksPeer::addInstanceToPool($obj, (string) $key);
+        }
+        $stmt->closeCursor();
+
+        return $obj;
+    }
+
+    /**
+     * Find object by primary key.
+     *
+     * @param     mixed $key Primary key to use for the query
+     * @param     PropelPDO $con A connection object
+     *
+     * @return CeleryTasks|CeleryTasks[]|mixed the result, formatted by the current formatter
+     */
+    protected function findPkComplex($key, $con)
+    {
+        // As the query uses a PK condition, no limit(1) is necessary.
+        $criteria = $this->isKeepQuery() ? clone $this : $this;
+        $stmt = $criteria
+            ->filterByPrimaryKey($key)
+            ->doSelect($con);
+
+        return $criteria->getFormatter()->init($criteria)->formatOne($stmt);
+    }
+
+    /**
+     * Find objects by primary key
+     * <code>
+     * $objs = $c->findPks(array(12, 56, 832), $con);
+     * </code>
+     * @param     array $keys Primary keys to use for the query
+     * @param     PropelPDO $con an optional connection object
+     *
+     * @return PropelObjectCollection|CeleryTasks[]|mixed the list of results, formatted by the current formatter
+     */
+    public function findPks($keys, $con = null)
+    {
+        if ($con === null) {
+            $con = Propel::getConnection($this->getDbName(), Propel::CONNECTION_READ);
+        }
+        $this->basePreSelect($con);
+        $criteria = $this->isKeepQuery() ? clone $this : $this;
+        $stmt = $criteria
+            ->filterByPrimaryKeys($keys)
+            ->doSelect($con);
+
+        return $criteria->getFormatter()->init($criteria)->format($stmt);
+    }
+
+    /**
+     * Filter the query by primary key
+     *
+     * @param     mixed $key Primary key to use for the query
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByPrimaryKey($key)
+    {
+
+        return $this->addUsingAlias(CeleryTasksPeer::ID, $key, Criteria::EQUAL);
+    }
+
+    /**
+     * Filter the query by a list of primary keys
+     *
+     * @param     array $keys The list of primary key to use for the query
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByPrimaryKeys($keys)
+    {
+
+        return $this->addUsingAlias(CeleryTasksPeer::ID, $keys, Criteria::IN);
+    }
+
+    /**
+     * Filter the query on the id column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbId('fooValue');   // WHERE id = 'fooValue'
+     * $query->filterByDbId('%fooValue%'); // WHERE id LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbId The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByDbId($dbId = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbId)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbId)) {
+                $dbId = str_replace('*', '%', $dbId);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(CeleryTasksPeer::ID, $dbId, $comparison);
+    }
+
+    /**
+     * Filter the query on the track_reference column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbTrackReference(1234); // WHERE track_reference = 1234
+     * $query->filterByDbTrackReference(array(12, 34)); // WHERE track_reference IN (12, 34)
+     * $query->filterByDbTrackReference(array('min' => 12)); // WHERE track_reference >= 12
+     * $query->filterByDbTrackReference(array('max' => 12)); // WHERE track_reference <= 12
+     * </code>
+     *
+     * @see       filterByThirdPartyTrackReferences()
+     *
+     * @param     mixed $dbTrackReference The value to use as filter.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByDbTrackReference($dbTrackReference = null, $comparison = null)
+    {
+        if (is_array($dbTrackReference)) {
+            $useMinMax = false;
+            if (isset($dbTrackReference['min'])) {
+                $this->addUsingAlias(CeleryTasksPeer::TRACK_REFERENCE, $dbTrackReference['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbTrackReference['max'])) {
+                $this->addUsingAlias(CeleryTasksPeer::TRACK_REFERENCE, $dbTrackReference['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+        }
+
+        return $this->addUsingAlias(CeleryTasksPeer::TRACK_REFERENCE, $dbTrackReference, $comparison);
+    }
+
+    /**
+     * Filter the query on the name column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbName('fooValue');   // WHERE name = 'fooValue'
+     * $query->filterByDbName('%fooValue%'); // WHERE name LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbName The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByDbName($dbName = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbName)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbName)) {
+                $dbName = str_replace('*', '%', $dbName);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(CeleryTasksPeer::NAME, $dbName, $comparison);
+    }
+
+    /**
+     * Filter the query on the dispatch_time column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbDispatchTime('2011-03-14'); // WHERE dispatch_time = '2011-03-14'
+     * $query->filterByDbDispatchTime('now'); // WHERE dispatch_time = '2011-03-14'
+     * $query->filterByDbDispatchTime(array('max' => 'yesterday')); // WHERE dispatch_time < '2011-03-13'
+     * </code>
+     *
+     * @param     mixed $dbDispatchTime The value to use as filter.
+     *              Values can be integers (unix timestamps), DateTime objects, or strings.
+     *              Empty strings are treated as NULL.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByDbDispatchTime($dbDispatchTime = null, $comparison = null)
+    {
+        if (is_array($dbDispatchTime)) {
+            $useMinMax = false;
+            if (isset($dbDispatchTime['min'])) {
+                $this->addUsingAlias(CeleryTasksPeer::DISPATCH_TIME, $dbDispatchTime['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbDispatchTime['max'])) {
+                $this->addUsingAlias(CeleryTasksPeer::DISPATCH_TIME, $dbDispatchTime['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+        }
+
+        return $this->addUsingAlias(CeleryTasksPeer::DISPATCH_TIME, $dbDispatchTime, $comparison);
+    }
+
+    /**
+     * Filter the query on the status column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbStatus('fooValue');   // WHERE status = 'fooValue'
+     * $query->filterByDbStatus('%fooValue%'); // WHERE status LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbStatus The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByDbStatus($dbStatus = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbStatus)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbStatus)) {
+                $dbStatus = str_replace('*', '%', $dbStatus);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(CeleryTasksPeer::STATUS, $dbStatus, $comparison);
+    }
+
+    /**
+     * Filter the query by a related ThirdPartyTrackReferences object
+     *
+     * @param   ThirdPartyTrackReferences|PropelObjectCollection $thirdPartyTrackReferences The related object(s) to use as filter
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return                 CeleryTasksQuery The current query, for fluid interface
+     * @throws PropelException - if the provided filter is invalid.
+     */
+    public function filterByThirdPartyTrackReferences($thirdPartyTrackReferences, $comparison = null)
+    {
+        if ($thirdPartyTrackReferences instanceof ThirdPartyTrackReferences) {
+            return $this
+                ->addUsingAlias(CeleryTasksPeer::TRACK_REFERENCE, $thirdPartyTrackReferences->getDbId(), $comparison);
+        } elseif ($thirdPartyTrackReferences instanceof PropelObjectCollection) {
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+
+            return $this
+                ->addUsingAlias(CeleryTasksPeer::TRACK_REFERENCE, $thirdPartyTrackReferences->toKeyValue('PrimaryKey', 'DbId'), $comparison);
+        } else {
+            throw new PropelException('filterByThirdPartyTrackReferences() only accepts arguments of type ThirdPartyTrackReferences or PropelCollection');
+        }
+    }
+
+    /**
+     * Adds a JOIN clause to the query using the ThirdPartyTrackReferences relation
+     *
+     * @param     string $relationAlias optional alias for the relation
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function joinThirdPartyTrackReferences($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        $tableMap = $this->getTableMap();
+        $relationMap = $tableMap->getRelation('ThirdPartyTrackReferences');
+
+        // create a ModelJoin object for this join
+        $join = new ModelJoin();
+        $join->setJoinType($joinType);
+        $join->setRelationMap($relationMap, $this->useAliasInSQL ? $this->getModelAlias() : null, $relationAlias);
+        if ($previousJoin = $this->getPreviousJoin()) {
+            $join->setPreviousJoin($previousJoin);
+        }
+
+        // add the ModelJoin to the current object
+        if ($relationAlias) {
+            $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
+            $this->addJoinObject($join, $relationAlias);
+        } else {
+            $this->addJoinObject($join, 'ThirdPartyTrackReferences');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Use the ThirdPartyTrackReferences relation ThirdPartyTrackReferences object
+     *
+     * @see       useQuery()
+     *
+     * @param     string $relationAlias optional alias for the relation,
+     *                                   to be used as main alias in the secondary query
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return   ThirdPartyTrackReferencesQuery A secondary query class using the current class as primary query
+     */
+    public function useThirdPartyTrackReferencesQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        return $this
+            ->joinThirdPartyTrackReferences($relationAlias, $joinType)
+            ->useQuery($relationAlias ? $relationAlias : 'ThirdPartyTrackReferences', 'ThirdPartyTrackReferencesQuery');
+    }
+
+    /**
+     * Exclude object from result
+     *
+     * @param   CeleryTasks $celeryTasks Object to remove from the list of results
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function prune($celeryTasks = null)
+    {
+        if ($celeryTasks) {
+            $this->addUsingAlias(CeleryTasksPeer::ID, $celeryTasks->getDbId(), Criteria::NOT_EQUAL);
+        }
+
+        return $this;
+    }
+
+}
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
index b880cd0d4..a7860e1eb 100644
--- a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferences.php
@@ -47,30 +47,18 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
      */
     protected $foreign_id;
 
-    /**
-     * The value for the broker_task_id field.
-     * @var        string
-     */
-    protected $broker_task_id;
-
-    /**
-     * The value for the broker_task_name field.
-     * @var        string
-     */
-    protected $broker_task_name;
-
-    /**
-     * The value for the broker_task_dispatch_time field.
-     * @var        string
-     */
-    protected $broker_task_dispatch_time;
-
     /**
      * The value for the file_id field.
      * @var        int
      */
     protected $file_id;
 
+    /**
+     * The value for the upload_time field.
+     * @var        string
+     */
+    protected $upload_time;
+
     /**
      * The value for the status field.
      * @var        string
@@ -78,9 +66,15 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     protected $status;
 
     /**
-     * @var        CcPlayoutHistoryTemplate
+     * @var        CcFiles
      */
-    protected $aCcPlayoutHistoryTemplate;
+    protected $aCcFiles;
+
+    /**
+     * @var        PropelObjectCollection|CeleryTasks[] Collection to store aggregation of CeleryTasks objects.
+     */
+    protected $collCeleryTaskss;
+    protected $collCeleryTaskssPartial;
 
     /**
      * Flag to prevent endless save loop, if this object is referenced
@@ -102,6 +96,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
      */
     protected $alreadyInClearAllReferencesDeep = false;
 
+    /**
+     * An array of objects scheduled for deletion.
+     * @var		PropelObjectCollection
+     */
+    protected $celeryTaskssScheduledForDeletion = null;
+
     /**
      * Get the [id] column value.
      *
@@ -136,29 +136,18 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     }
 
     /**
-     * Get the [broker_task_id] column value.
+     * Get the [file_id] column value.
      *
-     * @return string
+     * @return int
      */
-    public function getDbBrokerTaskId()
+    public function getDbFileId()
     {
 
-        return $this->broker_task_id;
+        return $this->file_id;
     }
 
     /**
-     * Get the [broker_task_name] column value.
-     *
-     * @return string
-     */
-    public function getDbBrokerTaskName()
-    {
-
-        return $this->broker_task_name;
-    }
-
-    /**
-     * Get the [optionally formatted] temporal [broker_task_dispatch_time] column value.
+     * Get the [optionally formatted] temporal [upload_time] column value.
      *
      *
      * @param string $format The date/time format string (either date()-style or strftime()-style).
@@ -166,17 +155,17 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
      * @return mixed Formatted date/time value as string or DateTime object (if format is null), null if column is null
      * @throws PropelException - if unable to parse/validate the date/time value.
      */
-    public function getDbBrokerTaskDispatchTime($format = 'Y-m-d H:i:s')
+    public function getDbUploadTime($format = 'Y-m-d H:i:s')
     {
-        if ($this->broker_task_dispatch_time === null) {
+        if ($this->upload_time === null) {
             return null;
         }
 
 
         try {
-            $dt = new DateTime($this->broker_task_dispatch_time);
+            $dt = new DateTime($this->upload_time);
         } catch (Exception $x) {
-            throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->broker_task_dispatch_time, true), $x);
+            throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->upload_time, true), $x);
         }
 
         if ($format === null) {
@@ -192,17 +181,6 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
 
     }
 
-    /**
-     * Get the [file_id] column value.
-     *
-     * @return int
-     */
-    public function getDbFileId()
-    {
-
-        return $this->file_id;
-    }
-
     /**
      * Get the [status] column value.
      *
@@ -277,71 +255,6 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         return $this;
     } // setDbForeignId()
 
-    /**
-     * Set the value of [broker_task_id] column.
-     *
-     * @param  string $v new value
-     * @return ThirdPartyTrackReferences The current object (for fluent API support)
-     */
-    public function setDbBrokerTaskId($v)
-    {
-        if ($v !== null && is_numeric($v)) {
-            $v = (string) $v;
-        }
-
-        if ($this->broker_task_id !== $v) {
-            $this->broker_task_id = $v;
-            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::BROKER_TASK_ID;
-        }
-
-
-        return $this;
-    } // setDbBrokerTaskId()
-
-    /**
-     * Set the value of [broker_task_name] column.
-     *
-     * @param  string $v new value
-     * @return ThirdPartyTrackReferences The current object (for fluent API support)
-     */
-    public function setDbBrokerTaskName($v)
-    {
-        if ($v !== null && is_numeric($v)) {
-            $v = (string) $v;
-        }
-
-        if ($this->broker_task_name !== $v) {
-            $this->broker_task_name = $v;
-            $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME;
-        }
-
-
-        return $this;
-    } // setDbBrokerTaskName()
-
-    /**
-     * Sets the value of [broker_task_dispatch_time] column to a normalized version of the date/time value specified.
-     *
-     * @param mixed $v string, integer (timestamp), or DateTime value.
-     *               Empty strings are treated as null.
-     * @return ThirdPartyTrackReferences The current object (for fluent API support)
-     */
-    public function setDbBrokerTaskDispatchTime($v)
-    {
-        $dt = PropelDateTime::newInstance($v, null, 'DateTime');
-        if ($this->broker_task_dispatch_time !== null || $dt !== null) {
-            $currentDateAsString = ($this->broker_task_dispatch_time !== null && $tmpDt = new DateTime($this->broker_task_dispatch_time)) ? $tmpDt->format('Y-m-d H:i:s') : null;
-            $newDateAsString = $dt ? $dt->format('Y-m-d H:i:s') : null;
-            if ($currentDateAsString !== $newDateAsString) {
-                $this->broker_task_dispatch_time = $newDateAsString;
-                $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME;
-            }
-        } // if either are not null
-
-
-        return $this;
-    } // setDbBrokerTaskDispatchTime()
-
     /**
      * Set the value of [file_id] column.
      *
@@ -359,14 +272,37 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::FILE_ID;
         }
 
-        if ($this->aCcPlayoutHistoryTemplate !== null && $this->aCcPlayoutHistoryTemplate->getDbId() !== $v) {
-            $this->aCcPlayoutHistoryTemplate = null;
+        if ($this->aCcFiles !== null && $this->aCcFiles->getDbId() !== $v) {
+            $this->aCcFiles = null;
         }
 
 
         return $this;
     } // setDbFileId()
 
+    /**
+     * Sets the value of [upload_time] column to a normalized version of the date/time value specified.
+     *
+     * @param mixed $v string, integer (timestamp), or DateTime value.
+     *               Empty strings are treated as null.
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setDbUploadTime($v)
+    {
+        $dt = PropelDateTime::newInstance($v, null, 'DateTime');
+        if ($this->upload_time !== null || $dt !== null) {
+            $currentDateAsString = ($this->upload_time !== null && $tmpDt = new DateTime($this->upload_time)) ? $tmpDt->format('Y-m-d H:i:s') : null;
+            $newDateAsString = $dt ? $dt->format('Y-m-d H:i:s') : null;
+            if ($currentDateAsString !== $newDateAsString) {
+                $this->upload_time = $newDateAsString;
+                $this->modifiedColumns[] = ThirdPartyTrackReferencesPeer::UPLOAD_TIME;
+            }
+        } // if either are not null
+
+
+        return $this;
+    } // setDbUploadTime()
+
     /**
      * Set the value of [status] column.
      *
@@ -423,11 +359,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             $this->id = ($row[$startcol + 0] !== null) ? (int) $row[$startcol + 0] : null;
             $this->service = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null;
             $this->foreign_id = ($row[$startcol + 2] !== null) ? (string) $row[$startcol + 2] : null;
-            $this->broker_task_id = ($row[$startcol + 3] !== null) ? (string) $row[$startcol + 3] : null;
-            $this->broker_task_name = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
-            $this->broker_task_dispatch_time = ($row[$startcol + 5] !== null) ? (string) $row[$startcol + 5] : null;
-            $this->file_id = ($row[$startcol + 6] !== null) ? (int) $row[$startcol + 6] : null;
-            $this->status = ($row[$startcol + 7] !== null) ? (string) $row[$startcol + 7] : null;
+            $this->file_id = ($row[$startcol + 3] !== null) ? (int) $row[$startcol + 3] : null;
+            $this->upload_time = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->status = ($row[$startcol + 5] !== null) ? (string) $row[$startcol + 5] : null;
             $this->resetModified();
 
             $this->setNew(false);
@@ -437,7 +371,7 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             }
             $this->postHydrate($row, $startcol, $rehydrate);
 
-            return $startcol + 8; // 8 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS.
+            return $startcol + 6; // 6 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS.
 
         } catch (Exception $e) {
             throw new PropelException("Error populating ThirdPartyTrackReferences object", $e);
@@ -460,8 +394,8 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     public function ensureConsistency()
     {
 
-        if ($this->aCcPlayoutHistoryTemplate !== null && $this->file_id !== $this->aCcPlayoutHistoryTemplate->getDbId()) {
-            $this->aCcPlayoutHistoryTemplate = null;
+        if ($this->aCcFiles !== null && $this->file_id !== $this->aCcFiles->getDbId()) {
+            $this->aCcFiles = null;
         }
     } // ensureConsistency
 
@@ -502,7 +436,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
 
         if ($deep) {  // also de-associate any related objects?
 
-            $this->aCcPlayoutHistoryTemplate = null;
+            $this->aCcFiles = null;
+            $this->collCeleryTaskss = null;
+
         } // if (deep)
     }
 
@@ -621,11 +557,11 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             // method.  This object relates to these object(s) by a
             // foreign key reference.
 
-            if ($this->aCcPlayoutHistoryTemplate !== null) {
-                if ($this->aCcPlayoutHistoryTemplate->isModified() || $this->aCcPlayoutHistoryTemplate->isNew()) {
-                    $affectedRows += $this->aCcPlayoutHistoryTemplate->save($con);
+            if ($this->aCcFiles !== null) {
+                if ($this->aCcFiles->isModified() || $this->aCcFiles->isNew()) {
+                    $affectedRows += $this->aCcFiles->save($con);
                 }
-                $this->setCcPlayoutHistoryTemplate($this->aCcPlayoutHistoryTemplate);
+                $this->setCcFiles($this->aCcFiles);
             }
 
             if ($this->isNew() || $this->isModified()) {
@@ -639,6 +575,23 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                 $this->resetModified();
             }
 
+            if ($this->celeryTaskssScheduledForDeletion !== null) {
+                if (!$this->celeryTaskssScheduledForDeletion->isEmpty()) {
+                    CeleryTasksQuery::create()
+                        ->filterByPrimaryKeys($this->celeryTaskssScheduledForDeletion->getPrimaryKeys(false))
+                        ->delete($con);
+                    $this->celeryTaskssScheduledForDeletion = null;
+                }
+            }
+
+            if ($this->collCeleryTaskss !== null) {
+                foreach ($this->collCeleryTaskss as $referrerFK) {
+                    if (!$referrerFK->isDeleted() && ($referrerFK->isNew() || $referrerFK->isModified())) {
+                        $affectedRows += $referrerFK->save($con);
+                    }
+                }
+            }
+
             $this->alreadyInSave = false;
 
         }
@@ -684,18 +637,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FOREIGN_ID)) {
             $modifiedColumns[':p' . $index++]  = '"foreign_id"';
         }
-        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID)) {
-            $modifiedColumns[':p' . $index++]  = '"broker_task_id"';
-        }
-        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME)) {
-            $modifiedColumns[':p' . $index++]  = '"broker_task_name"';
-        }
-        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME)) {
-            $modifiedColumns[':p' . $index++]  = '"broker_task_dispatch_time"';
-        }
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FILE_ID)) {
             $modifiedColumns[':p' . $index++]  = '"file_id"';
         }
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::UPLOAD_TIME)) {
+            $modifiedColumns[':p' . $index++]  = '"upload_time"';
+        }
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::STATUS)) {
             $modifiedColumns[':p' . $index++]  = '"status"';
         }
@@ -719,18 +666,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                     case '"foreign_id"':
                         $stmt->bindValue($identifier, $this->foreign_id, PDO::PARAM_STR);
                         break;
-                    case '"broker_task_id"':
-                        $stmt->bindValue($identifier, $this->broker_task_id, PDO::PARAM_STR);
-                        break;
-                    case '"broker_task_name"':
-                        $stmt->bindValue($identifier, $this->broker_task_name, PDO::PARAM_STR);
-                        break;
-                    case '"broker_task_dispatch_time"':
-                        $stmt->bindValue($identifier, $this->broker_task_dispatch_time, PDO::PARAM_STR);
-                        break;
                     case '"file_id"':
                         $stmt->bindValue($identifier, $this->file_id, PDO::PARAM_INT);
                         break;
+                    case '"upload_time"':
+                        $stmt->bindValue($identifier, $this->upload_time, PDO::PARAM_STR);
+                        break;
                     case '"status"':
                         $stmt->bindValue($identifier, $this->status, PDO::PARAM_STR);
                         break;
@@ -826,9 +767,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             // method.  This object relates to these object(s) by a
             // foreign key reference.
 
-            if ($this->aCcPlayoutHistoryTemplate !== null) {
-                if (!$this->aCcPlayoutHistoryTemplate->validate($columns)) {
-                    $failureMap = array_merge($failureMap, $this->aCcPlayoutHistoryTemplate->getValidationFailures());
+            if ($this->aCcFiles !== null) {
+                if (!$this->aCcFiles->validate($columns)) {
+                    $failureMap = array_merge($failureMap, $this->aCcFiles->getValidationFailures());
                 }
             }
 
@@ -838,6 +779,14 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             }
 
 
+                if ($this->collCeleryTaskss !== null) {
+                    foreach ($this->collCeleryTaskss as $referrerFK) {
+                        if (!$referrerFK->validate($columns)) {
+                            $failureMap = array_merge($failureMap, $referrerFK->getValidationFailures());
+                        }
+                    }
+                }
+
 
             $this->alreadyInValidation = false;
         }
@@ -883,18 +832,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                 return $this->getDbForeignId();
                 break;
             case 3:
-                return $this->getDbBrokerTaskId();
-                break;
-            case 4:
-                return $this->getDbBrokerTaskName();
-                break;
-            case 5:
-                return $this->getDbBrokerTaskDispatchTime();
-                break;
-            case 6:
                 return $this->getDbFileId();
                 break;
-            case 7:
+            case 4:
+                return $this->getDbUploadTime();
+                break;
+            case 5:
                 return $this->getDbStatus();
                 break;
             default:
@@ -929,11 +872,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             $keys[0] => $this->getDbId(),
             $keys[1] => $this->getDbService(),
             $keys[2] => $this->getDbForeignId(),
-            $keys[3] => $this->getDbBrokerTaskId(),
-            $keys[4] => $this->getDbBrokerTaskName(),
-            $keys[5] => $this->getDbBrokerTaskDispatchTime(),
-            $keys[6] => $this->getDbFileId(),
-            $keys[7] => $this->getDbStatus(),
+            $keys[3] => $this->getDbFileId(),
+            $keys[4] => $this->getDbUploadTime(),
+            $keys[5] => $this->getDbStatus(),
         );
         $virtualColumns = $this->virtualColumns;
         foreach ($virtualColumns as $key => $virtualColumn) {
@@ -941,8 +882,11 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         }
 
         if ($includeForeignObjects) {
-            if (null !== $this->aCcPlayoutHistoryTemplate) {
-                $result['CcPlayoutHistoryTemplate'] = $this->aCcPlayoutHistoryTemplate->toArray($keyType, $includeLazyLoadColumns,  $alreadyDumpedObjects, true);
+            if (null !== $this->aCcFiles) {
+                $result['CcFiles'] = $this->aCcFiles->toArray($keyType, $includeLazyLoadColumns,  $alreadyDumpedObjects, true);
+            }
+            if (null !== $this->collCeleryTaskss) {
+                $result['CeleryTaskss'] = $this->collCeleryTaskss->toArray(null, true, $keyType, $includeLazyLoadColumns, $alreadyDumpedObjects);
             }
         }
 
@@ -988,18 +932,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
                 $this->setDbForeignId($value);
                 break;
             case 3:
-                $this->setDbBrokerTaskId($value);
-                break;
-            case 4:
-                $this->setDbBrokerTaskName($value);
-                break;
-            case 5:
-                $this->setDbBrokerTaskDispatchTime($value);
-                break;
-            case 6:
                 $this->setDbFileId($value);
                 break;
-            case 7:
+            case 4:
+                $this->setDbUploadTime($value);
+                break;
+            case 5:
                 $this->setDbStatus($value);
                 break;
         } // switch()
@@ -1029,11 +967,9 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         if (array_key_exists($keys[0], $arr)) $this->setDbId($arr[$keys[0]]);
         if (array_key_exists($keys[1], $arr)) $this->setDbService($arr[$keys[1]]);
         if (array_key_exists($keys[2], $arr)) $this->setDbForeignId($arr[$keys[2]]);
-        if (array_key_exists($keys[3], $arr)) $this->setDbBrokerTaskId($arr[$keys[3]]);
-        if (array_key_exists($keys[4], $arr)) $this->setDbBrokerTaskName($arr[$keys[4]]);
-        if (array_key_exists($keys[5], $arr)) $this->setDbBrokerTaskDispatchTime($arr[$keys[5]]);
-        if (array_key_exists($keys[6], $arr)) $this->setDbFileId($arr[$keys[6]]);
-        if (array_key_exists($keys[7], $arr)) $this->setDbStatus($arr[$keys[7]]);
+        if (array_key_exists($keys[3], $arr)) $this->setDbFileId($arr[$keys[3]]);
+        if (array_key_exists($keys[4], $arr)) $this->setDbUploadTime($arr[$keys[4]]);
+        if (array_key_exists($keys[5], $arr)) $this->setDbStatus($arr[$keys[5]]);
     }
 
     /**
@@ -1048,10 +984,8 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::ID)) $criteria->add(ThirdPartyTrackReferencesPeer::ID, $this->id);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::SERVICE)) $criteria->add(ThirdPartyTrackReferencesPeer::SERVICE, $this->service);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FOREIGN_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $this->foreign_id);
-        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID, $this->broker_task_id);
-        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME)) $criteria->add(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME, $this->broker_task_name);
-        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME)) $criteria->add(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $this->broker_task_dispatch_time);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::FILE_ID)) $criteria->add(ThirdPartyTrackReferencesPeer::FILE_ID, $this->file_id);
+        if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::UPLOAD_TIME)) $criteria->add(ThirdPartyTrackReferencesPeer::UPLOAD_TIME, $this->upload_time);
         if ($this->isColumnModified(ThirdPartyTrackReferencesPeer::STATUS)) $criteria->add(ThirdPartyTrackReferencesPeer::STATUS, $this->status);
 
         return $criteria;
@@ -1118,10 +1052,8 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     {
         $copyObj->setDbService($this->getDbService());
         $copyObj->setDbForeignId($this->getDbForeignId());
-        $copyObj->setDbBrokerTaskId($this->getDbBrokerTaskId());
-        $copyObj->setDbBrokerTaskName($this->getDbBrokerTaskName());
-        $copyObj->setDbBrokerTaskDispatchTime($this->getDbBrokerTaskDispatchTime());
         $copyObj->setDbFileId($this->getDbFileId());
+        $copyObj->setDbUploadTime($this->getDbUploadTime());
         $copyObj->setDbStatus($this->getDbStatus());
 
         if ($deepCopy && !$this->startCopy) {
@@ -1131,6 +1063,12 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             // store object hash to prevent cycle
             $this->startCopy = true;
 
+            foreach ($this->getCeleryTaskss() as $relObj) {
+                if ($relObj !== $this) {  // ensure that we don't try to copy a reference to ourselves
+                    $copyObj->addCeleryTasks($relObj->copy($deepCopy));
+                }
+            }
+
             //unflag object copy
             $this->startCopy = false;
         } // if ($deepCopy)
@@ -1182,13 +1120,13 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     }
 
     /**
-     * Declares an association between this object and a CcPlayoutHistoryTemplate object.
+     * Declares an association between this object and a CcFiles object.
      *
-     * @param                  CcPlayoutHistoryTemplate $v
+     * @param                  CcFiles $v
      * @return ThirdPartyTrackReferences The current object (for fluent API support)
      * @throws PropelException
      */
-    public function setCcPlayoutHistoryTemplate(CcPlayoutHistoryTemplate $v = null)
+    public function setCcFiles(CcFiles $v = null)
     {
         if ($v === null) {
             $this->setDbFileId(NULL);
@@ -1196,10 +1134,10 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
             $this->setDbFileId($v->getDbId());
         }
 
-        $this->aCcPlayoutHistoryTemplate = $v;
+        $this->aCcFiles = $v;
 
         // Add binding for other direction of this n:n relationship.
-        // If this object has already been added to the CcPlayoutHistoryTemplate object, it will not be re-added.
+        // If this object has already been added to the CcFiles object, it will not be re-added.
         if ($v !== null) {
             $v->addThirdPartyTrackReferences($this);
         }
@@ -1210,27 +1148,268 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
 
 
     /**
-     * Get the associated CcPlayoutHistoryTemplate object
+     * Get the associated CcFiles object
      *
      * @param PropelPDO $con Optional Connection object.
      * @param $doQuery Executes a query to get the object if required
-     * @return CcPlayoutHistoryTemplate The associated CcPlayoutHistoryTemplate object.
+     * @return CcFiles The associated CcFiles object.
      * @throws PropelException
      */
-    public function getCcPlayoutHistoryTemplate(PropelPDO $con = null, $doQuery = true)
+    public function getCcFiles(PropelPDO $con = null, $doQuery = true)
     {
-        if ($this->aCcPlayoutHistoryTemplate === null && ($this->file_id !== null) && $doQuery) {
-            $this->aCcPlayoutHistoryTemplate = CcPlayoutHistoryTemplateQuery::create()->findPk($this->file_id, $con);
+        if ($this->aCcFiles === null && ($this->file_id !== null) && $doQuery) {
+            $this->aCcFiles = CcFilesQuery::create()->findPk($this->file_id, $con);
             /* The following can be used additionally to
                 guarantee the related object contains a reference
                 to this object.  This level of coupling may, however, be
                 undesirable since it could result in an only partially populated collection
                 in the referenced object.
-                $this->aCcPlayoutHistoryTemplate->addThirdPartyTrackReferencess($this);
+                $this->aCcFiles->addThirdPartyTrackReferencess($this);
              */
         }
 
-        return $this->aCcPlayoutHistoryTemplate;
+        return $this->aCcFiles;
+    }
+
+
+    /**
+     * Initializes a collection based on the name of a relation.
+     * Avoids crafting an 'init[$relationName]s' method name
+     * that wouldn't work when StandardEnglishPluralizer is used.
+     *
+     * @param string $relationName The name of the relation to initialize
+     * @return void
+     */
+    public function initRelation($relationName)
+    {
+        if ('CeleryTasks' == $relationName) {
+            $this->initCeleryTaskss();
+        }
+    }
+
+    /**
+     * Clears out the collCeleryTaskss collection
+     *
+     * This does not modify the database; however, it will remove any associated objects, causing
+     * them to be refetched by subsequent calls to accessor method.
+     *
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     * @see        addCeleryTaskss()
+     */
+    public function clearCeleryTaskss()
+    {
+        $this->collCeleryTaskss = null; // important to set this to null since that means it is uninitialized
+        $this->collCeleryTaskssPartial = null;
+
+        return $this;
+    }
+
+    /**
+     * reset is the collCeleryTaskss collection loaded partially
+     *
+     * @return void
+     */
+    public function resetPartialCeleryTaskss($v = true)
+    {
+        $this->collCeleryTaskssPartial = $v;
+    }
+
+    /**
+     * Initializes the collCeleryTaskss collection.
+     *
+     * By default this just sets the collCeleryTaskss collection to an empty array (like clearcollCeleryTaskss());
+     * however, you may wish to override this method in your stub class to provide setting appropriate
+     * to your application -- for example, setting the initial array to the values stored in database.
+     *
+     * @param boolean $overrideExisting If set to true, the method call initializes
+     *                                        the collection even if it is not empty
+     *
+     * @return void
+     */
+    public function initCeleryTaskss($overrideExisting = true)
+    {
+        if (null !== $this->collCeleryTaskss && !$overrideExisting) {
+            return;
+        }
+        $this->collCeleryTaskss = new PropelObjectCollection();
+        $this->collCeleryTaskss->setModel('CeleryTasks');
+    }
+
+    /**
+     * Gets an array of CeleryTasks objects which contain a foreign key that references this object.
+     *
+     * If the $criteria is not null, it is used to always fetch the results from the database.
+     * Otherwise the results are fetched from the database the first time, then cached.
+     * Next time the same method is called without $criteria, the cached collection is returned.
+     * If this ThirdPartyTrackReferences is new, it will return
+     * an empty collection or the current collection; the criteria is ignored on a new object.
+     *
+     * @param Criteria $criteria optional Criteria object to narrow the query
+     * @param PropelPDO $con optional connection object
+     * @return PropelObjectCollection|CeleryTasks[] List of CeleryTasks objects
+     * @throws PropelException
+     */
+    public function getCeleryTaskss($criteria = null, PropelPDO $con = null)
+    {
+        $partial = $this->collCeleryTaskssPartial && !$this->isNew();
+        if (null === $this->collCeleryTaskss || null !== $criteria  || $partial) {
+            if ($this->isNew() && null === $this->collCeleryTaskss) {
+                // return empty collection
+                $this->initCeleryTaskss();
+            } else {
+                $collCeleryTaskss = CeleryTasksQuery::create(null, $criteria)
+                    ->filterByThirdPartyTrackReferences($this)
+                    ->find($con);
+                if (null !== $criteria) {
+                    if (false !== $this->collCeleryTaskssPartial && count($collCeleryTaskss)) {
+                      $this->initCeleryTaskss(false);
+
+                      foreach ($collCeleryTaskss as $obj) {
+                        if (false == $this->collCeleryTaskss->contains($obj)) {
+                          $this->collCeleryTaskss->append($obj);
+                        }
+                      }
+
+                      $this->collCeleryTaskssPartial = true;
+                    }
+
+                    $collCeleryTaskss->getInternalIterator()->rewind();
+
+                    return $collCeleryTaskss;
+                }
+
+                if ($partial && $this->collCeleryTaskss) {
+                    foreach ($this->collCeleryTaskss as $obj) {
+                        if ($obj->isNew()) {
+                            $collCeleryTaskss[] = $obj;
+                        }
+                    }
+                }
+
+                $this->collCeleryTaskss = $collCeleryTaskss;
+                $this->collCeleryTaskssPartial = false;
+            }
+        }
+
+        return $this->collCeleryTaskss;
+    }
+
+    /**
+     * Sets a collection of CeleryTasks objects related by a one-to-many relationship
+     * to the current object.
+     * It will also schedule objects for deletion based on a diff between old objects (aka persisted)
+     * and new objects from the given Propel collection.
+     *
+     * @param PropelCollection $celeryTaskss A Propel collection.
+     * @param PropelPDO $con Optional connection object
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function setCeleryTaskss(PropelCollection $celeryTaskss, PropelPDO $con = null)
+    {
+        $celeryTaskssToDelete = $this->getCeleryTaskss(new Criteria(), $con)->diff($celeryTaskss);
+
+
+        $this->celeryTaskssScheduledForDeletion = $celeryTaskssToDelete;
+
+        foreach ($celeryTaskssToDelete as $celeryTasksRemoved) {
+            $celeryTasksRemoved->setThirdPartyTrackReferences(null);
+        }
+
+        $this->collCeleryTaskss = null;
+        foreach ($celeryTaskss as $celeryTasks) {
+            $this->addCeleryTasks($celeryTasks);
+        }
+
+        $this->collCeleryTaskss = $celeryTaskss;
+        $this->collCeleryTaskssPartial = false;
+
+        return $this;
+    }
+
+    /**
+     * Returns the number of related CeleryTasks objects.
+     *
+     * @param Criteria $criteria
+     * @param boolean $distinct
+     * @param PropelPDO $con
+     * @return int             Count of related CeleryTasks objects.
+     * @throws PropelException
+     */
+    public function countCeleryTaskss(Criteria $criteria = null, $distinct = false, PropelPDO $con = null)
+    {
+        $partial = $this->collCeleryTaskssPartial && !$this->isNew();
+        if (null === $this->collCeleryTaskss || null !== $criteria || $partial) {
+            if ($this->isNew() && null === $this->collCeleryTaskss) {
+                return 0;
+            }
+
+            if ($partial && !$criteria) {
+                return count($this->getCeleryTaskss());
+            }
+            $query = CeleryTasksQuery::create(null, $criteria);
+            if ($distinct) {
+                $query->distinct();
+            }
+
+            return $query
+                ->filterByThirdPartyTrackReferences($this)
+                ->count($con);
+        }
+
+        return count($this->collCeleryTaskss);
+    }
+
+    /**
+     * Method called to associate a CeleryTasks object to this object
+     * through the CeleryTasks foreign key attribute.
+     *
+     * @param    CeleryTasks $l CeleryTasks
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function addCeleryTasks(CeleryTasks $l)
+    {
+        if ($this->collCeleryTaskss === null) {
+            $this->initCeleryTaskss();
+            $this->collCeleryTaskssPartial = true;
+        }
+
+        if (!in_array($l, $this->collCeleryTaskss->getArrayCopy(), true)) { // only add it if the **same** object is not already associated
+            $this->doAddCeleryTasks($l);
+
+            if ($this->celeryTaskssScheduledForDeletion and $this->celeryTaskssScheduledForDeletion->contains($l)) {
+                $this->celeryTaskssScheduledForDeletion->remove($this->celeryTaskssScheduledForDeletion->search($l));
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param	CeleryTasks $celeryTasks The celeryTasks object to add.
+     */
+    protected function doAddCeleryTasks($celeryTasks)
+    {
+        $this->collCeleryTaskss[]= $celeryTasks;
+        $celeryTasks->setThirdPartyTrackReferences($this);
+    }
+
+    /**
+     * @param	CeleryTasks $celeryTasks The celeryTasks object to remove.
+     * @return ThirdPartyTrackReferences The current object (for fluent API support)
+     */
+    public function removeCeleryTasks($celeryTasks)
+    {
+        if ($this->getCeleryTaskss()->contains($celeryTasks)) {
+            $this->collCeleryTaskss->remove($this->collCeleryTaskss->search($celeryTasks));
+            if (null === $this->celeryTaskssScheduledForDeletion) {
+                $this->celeryTaskssScheduledForDeletion = clone $this->collCeleryTaskss;
+                $this->celeryTaskssScheduledForDeletion->clear();
+            }
+            $this->celeryTaskssScheduledForDeletion[]= clone $celeryTasks;
+            $celeryTasks->setThirdPartyTrackReferences(null);
+        }
+
+        return $this;
     }
 
     /**
@@ -1241,10 +1420,8 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
         $this->id = null;
         $this->service = null;
         $this->foreign_id = null;
-        $this->broker_task_id = null;
-        $this->broker_task_name = null;
-        $this->broker_task_dispatch_time = null;
         $this->file_id = null;
+        $this->upload_time = null;
         $this->status = null;
         $this->alreadyInSave = false;
         $this->alreadyInValidation = false;
@@ -1268,14 +1445,23 @@ abstract class BaseThirdPartyTrackReferences extends BaseObject implements Persi
     {
         if ($deep && !$this->alreadyInClearAllReferencesDeep) {
             $this->alreadyInClearAllReferencesDeep = true;
-            if ($this->aCcPlayoutHistoryTemplate instanceof Persistent) {
-              $this->aCcPlayoutHistoryTemplate->clearAllReferences($deep);
+            if ($this->collCeleryTaskss) {
+                foreach ($this->collCeleryTaskss as $o) {
+                    $o->clearAllReferences($deep);
+                }
+            }
+            if ($this->aCcFiles instanceof Persistent) {
+              $this->aCcFiles->clearAllReferences($deep);
             }
 
             $this->alreadyInClearAllReferencesDeep = false;
         } // if ($deep)
 
-        $this->aCcPlayoutHistoryTemplate = null;
+        if ($this->collCeleryTaskss instanceof PropelCollection) {
+            $this->collCeleryTaskss->clearIterator();
+        }
+        $this->collCeleryTaskss = null;
+        $this->aCcFiles = null;
     }
 
     /**
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
index 079dd9808..38c8ddbd9 100644
--- a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesPeer.php
@@ -24,13 +24,13 @@ abstract class BaseThirdPartyTrackReferencesPeer
     const TM_CLASS = 'ThirdPartyTrackReferencesTableMap';
 
     /** The total number of columns. */
-    const NUM_COLUMNS = 8;
+    const NUM_COLUMNS = 6;
 
     /** The number of lazy-loaded columns. */
     const NUM_LAZY_LOAD_COLUMNS = 0;
 
     /** The number of columns to hydrate (NUM_COLUMNS - NUM_LAZY_LOAD_COLUMNS) */
-    const NUM_HYDRATE_COLUMNS = 8;
+    const NUM_HYDRATE_COLUMNS = 6;
 
     /** the column name for the id field */
     const ID = 'third_party_track_references.id';
@@ -41,18 +41,12 @@ abstract class BaseThirdPartyTrackReferencesPeer
     /** the column name for the foreign_id field */
     const FOREIGN_ID = 'third_party_track_references.foreign_id';
 
-    /** the column name for the broker_task_id field */
-    const BROKER_TASK_ID = 'third_party_track_references.broker_task_id';
-
-    /** the column name for the broker_task_name field */
-    const BROKER_TASK_NAME = 'third_party_track_references.broker_task_name';
-
-    /** the column name for the broker_task_dispatch_time field */
-    const BROKER_TASK_DISPATCH_TIME = 'third_party_track_references.broker_task_dispatch_time';
-
     /** the column name for the file_id field */
     const FILE_ID = 'third_party_track_references.file_id';
 
+    /** the column name for the upload_time field */
+    const UPLOAD_TIME = 'third_party_track_references.upload_time';
+
     /** the column name for the status field */
     const STATUS = 'third_party_track_references.status';
 
@@ -75,12 +69,12 @@ abstract class BaseThirdPartyTrackReferencesPeer
      * e.g. ThirdPartyTrackReferencesPeer::$fieldNames[ThirdPartyTrackReferencesPeer::TYPE_PHPNAME][0] = 'Id'
      */
     protected static $fieldNames = array (
-        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbService', 'DbForeignId', 'DbBrokerTaskId', 'DbBrokerTaskName', 'DbBrokerTaskDispatchTime', 'DbFileId', 'DbStatus', ),
-        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbService', 'dbForeignId', 'dbBrokerTaskId', 'dbBrokerTaskName', 'dbBrokerTaskDispatchTime', 'dbFileId', 'dbStatus', ),
-        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID, ThirdPartyTrackReferencesPeer::SERVICE, ThirdPartyTrackReferencesPeer::FOREIGN_ID, ThirdPartyTrackReferencesPeer::BROKER_TASK_ID, ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME, ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, ThirdPartyTrackReferencesPeer::FILE_ID, ThirdPartyTrackReferencesPeer::STATUS, ),
-        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'SERVICE', 'FOREIGN_ID', 'BROKER_TASK_ID', 'BROKER_TASK_NAME', 'BROKER_TASK_DISPATCH_TIME', 'FILE_ID', 'STATUS', ),
-        BasePeer::TYPE_FIELDNAME => array ('id', 'service', 'foreign_id', 'broker_task_id', 'broker_task_name', 'broker_task_dispatch_time', 'file_id', 'status', ),
-        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, )
+        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbService', 'DbForeignId', 'DbFileId', 'DbUploadTime', 'DbStatus', ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbService', 'dbForeignId', 'dbFileId', 'dbUploadTime', 'dbStatus', ),
+        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID, ThirdPartyTrackReferencesPeer::SERVICE, ThirdPartyTrackReferencesPeer::FOREIGN_ID, ThirdPartyTrackReferencesPeer::FILE_ID, ThirdPartyTrackReferencesPeer::UPLOAD_TIME, ThirdPartyTrackReferencesPeer::STATUS, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'SERVICE', 'FOREIGN_ID', 'FILE_ID', 'UPLOAD_TIME', 'STATUS', ),
+        BasePeer::TYPE_FIELDNAME => array ('id', 'service', 'foreign_id', 'file_id', 'upload_time', 'status', ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, )
     );
 
     /**
@@ -90,12 +84,12 @@ abstract class BaseThirdPartyTrackReferencesPeer
      * e.g. ThirdPartyTrackReferencesPeer::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0
      */
     protected static $fieldKeys = array (
-        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbService' => 1, 'DbForeignId' => 2, 'DbBrokerTaskId' => 3, 'DbBrokerTaskName' => 4, 'DbBrokerTaskDispatchTime' => 5, 'DbFileId' => 6, 'DbStatus' => 7, ),
-        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbService' => 1, 'dbForeignId' => 2, 'dbBrokerTaskId' => 3, 'dbBrokerTaskName' => 4, 'dbBrokerTaskDispatchTime' => 5, 'dbFileId' => 6, 'dbStatus' => 7, ),
-        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID => 0, ThirdPartyTrackReferencesPeer::SERVICE => 1, ThirdPartyTrackReferencesPeer::FOREIGN_ID => 2, ThirdPartyTrackReferencesPeer::BROKER_TASK_ID => 3, ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME => 4, ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME => 5, ThirdPartyTrackReferencesPeer::FILE_ID => 6, ThirdPartyTrackReferencesPeer::STATUS => 7, ),
-        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'SERVICE' => 1, 'FOREIGN_ID' => 2, 'BROKER_TASK_ID' => 3, 'BROKER_TASK_NAME' => 4, 'BROKER_TASK_DISPATCH_TIME' => 5, 'FILE_ID' => 6, 'STATUS' => 7, ),
-        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'service' => 1, 'foreign_id' => 2, 'broker_task_id' => 3, 'broker_task_name' => 4, 'broker_task_dispatch_time' => 5, 'file_id' => 6, 'status' => 7, ),
-        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, )
+        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbService' => 1, 'DbForeignId' => 2, 'DbFileId' => 3, 'DbUploadTime' => 4, 'DbStatus' => 5, ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbService' => 1, 'dbForeignId' => 2, 'dbFileId' => 3, 'dbUploadTime' => 4, 'dbStatus' => 5, ),
+        BasePeer::TYPE_COLNAME => array (ThirdPartyTrackReferencesPeer::ID => 0, ThirdPartyTrackReferencesPeer::SERVICE => 1, ThirdPartyTrackReferencesPeer::FOREIGN_ID => 2, ThirdPartyTrackReferencesPeer::FILE_ID => 3, ThirdPartyTrackReferencesPeer::UPLOAD_TIME => 4, ThirdPartyTrackReferencesPeer::STATUS => 5, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'SERVICE' => 1, 'FOREIGN_ID' => 2, 'FILE_ID' => 3, 'UPLOAD_TIME' => 4, 'STATUS' => 5, ),
+        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'service' => 1, 'foreign_id' => 2, 'file_id' => 3, 'upload_time' => 4, 'status' => 5, ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, )
     );
 
     /**
@@ -172,19 +166,15 @@ abstract class BaseThirdPartyTrackReferencesPeer
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::ID);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::SERVICE);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::FOREIGN_ID);
-            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID);
-            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME);
-            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::FILE_ID);
+            $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::UPLOAD_TIME);
             $criteria->addSelectColumn(ThirdPartyTrackReferencesPeer::STATUS);
         } else {
             $criteria->addSelectColumn($alias . '.id');
             $criteria->addSelectColumn($alias . '.service');
             $criteria->addSelectColumn($alias . '.foreign_id');
-            $criteria->addSelectColumn($alias . '.broker_task_id');
-            $criteria->addSelectColumn($alias . '.broker_task_name');
-            $criteria->addSelectColumn($alias . '.broker_task_dispatch_time');
             $criteria->addSelectColumn($alias . '.file_id');
+            $criteria->addSelectColumn($alias . '.upload_time');
             $criteria->addSelectColumn($alias . '.status');
         }
     }
@@ -390,6 +380,9 @@ abstract class BaseThirdPartyTrackReferencesPeer
      */
     public static function clearRelatedInstancePool()
     {
+        // Invalidate objects in CeleryTasksPeer instance pool,
+        // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
+        CeleryTasksPeer::clearInstancePool();
     }
 
     /**
@@ -488,7 +481,7 @@ abstract class BaseThirdPartyTrackReferencesPeer
 
 
     /**
-     * Returns the number of rows matching criteria, joining the related CcPlayoutHistoryTemplate table
+     * Returns the number of rows matching criteria, joining the related CcFiles table
      *
      * @param      Criteria $criteria
      * @param      boolean $distinct Whether to select only distinct columns; deprecated: use Criteria->setDistinct() instead.
@@ -496,7 +489,7 @@ abstract class BaseThirdPartyTrackReferencesPeer
      * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
      * @return int Number of matching rows.
      */
-    public static function doCountJoinCcPlayoutHistoryTemplate(Criteria $criteria, $distinct = false, PropelPDO $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    public static function doCountJoinCcFiles(Criteria $criteria, $distinct = false, PropelPDO $con = null, $join_behavior = Criteria::LEFT_JOIN)
     {
         // we're going to modify criteria, so copy it first
         $criteria = clone $criteria;
@@ -523,7 +516,7 @@ abstract class BaseThirdPartyTrackReferencesPeer
             $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
         }
 
-        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcFilesPeer::ID, $join_behavior);
 
         $stmt = BasePeer::doCount($criteria, $con);
 
@@ -539,7 +532,7 @@ abstract class BaseThirdPartyTrackReferencesPeer
 
 
     /**
-     * Selects a collection of ThirdPartyTrackReferences objects pre-filled with their CcPlayoutHistoryTemplate objects.
+     * Selects a collection of ThirdPartyTrackReferences objects pre-filled with their CcFiles objects.
      * @param      Criteria  $criteria
      * @param      PropelPDO $con
      * @param      String    $join_behavior the type of joins to use, defaults to Criteria::LEFT_JOIN
@@ -547,7 +540,7 @@ abstract class BaseThirdPartyTrackReferencesPeer
      * @throws PropelException Any exceptions caught during processing will be
      *		 rethrown wrapped into a PropelException.
      */
-    public static function doSelectJoinCcPlayoutHistoryTemplate(Criteria $criteria, $con = null, $join_behavior = Criteria::LEFT_JOIN)
+    public static function doSelectJoinCcFiles(Criteria $criteria, $con = null, $join_behavior = Criteria::LEFT_JOIN)
     {
         $criteria = clone $criteria;
 
@@ -558,9 +551,9 @@ abstract class BaseThirdPartyTrackReferencesPeer
 
         ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
         $startcol = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS;
-        CcPlayoutHistoryTemplatePeer::addSelectColumns($criteria);
+        CcFilesPeer::addSelectColumns($criteria);
 
-        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcFilesPeer::ID, $join_behavior);
 
         $stmt = BasePeer::doSelect($criteria, $con);
         $results = array();
@@ -580,19 +573,19 @@ abstract class BaseThirdPartyTrackReferencesPeer
                 ThirdPartyTrackReferencesPeer::addInstanceToPool($obj1, $key1);
             } // if $obj1 already loaded
 
-            $key2 = CcPlayoutHistoryTemplatePeer::getPrimaryKeyHashFromRow($row, $startcol);
+            $key2 = CcFilesPeer::getPrimaryKeyHashFromRow($row, $startcol);
             if ($key2 !== null) {
-                $obj2 = CcPlayoutHistoryTemplatePeer::getInstanceFromPool($key2);
+                $obj2 = CcFilesPeer::getInstanceFromPool($key2);
                 if (!$obj2) {
 
-                    $cls = CcPlayoutHistoryTemplatePeer::getOMClass();
+                    $cls = CcFilesPeer::getOMClass();
 
                     $obj2 = new $cls();
                     $obj2->hydrate($row, $startcol);
-                    CcPlayoutHistoryTemplatePeer::addInstanceToPool($obj2, $key2);
+                    CcFilesPeer::addInstanceToPool($obj2, $key2);
                 } // if obj2 already loaded
 
-                // Add the $obj1 (ThirdPartyTrackReferences) to $obj2 (CcPlayoutHistoryTemplate)
+                // Add the $obj1 (ThirdPartyTrackReferences) to $obj2 (CcFiles)
                 $obj2->addThirdPartyTrackReferences($obj1);
 
             } // if joined row was not null
@@ -641,7 +634,7 @@ abstract class BaseThirdPartyTrackReferencesPeer
             $con = Propel::getConnection(ThirdPartyTrackReferencesPeer::DATABASE_NAME, Propel::CONNECTION_READ);
         }
 
-        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcFilesPeer::ID, $join_behavior);
 
         $stmt = BasePeer::doCount($criteria, $con);
 
@@ -677,10 +670,10 @@ abstract class BaseThirdPartyTrackReferencesPeer
         ThirdPartyTrackReferencesPeer::addSelectColumns($criteria);
         $startcol2 = ThirdPartyTrackReferencesPeer::NUM_HYDRATE_COLUMNS;
 
-        CcPlayoutHistoryTemplatePeer::addSelectColumns($criteria);
-        $startcol3 = $startcol2 + CcPlayoutHistoryTemplatePeer::NUM_HYDRATE_COLUMNS;
+        CcFilesPeer::addSelectColumns($criteria);
+        $startcol3 = $startcol2 + CcFilesPeer::NUM_HYDRATE_COLUMNS;
 
-        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcPlayoutHistoryTemplatePeer::ID, $join_behavior);
+        $criteria->addJoin(ThirdPartyTrackReferencesPeer::FILE_ID, CcFilesPeer::ID, $join_behavior);
 
         $stmt = BasePeer::doSelect($criteria, $con);
         $results = array();
@@ -699,21 +692,21 @@ abstract class BaseThirdPartyTrackReferencesPeer
                 ThirdPartyTrackReferencesPeer::addInstanceToPool($obj1, $key1);
             } // if obj1 already loaded
 
-            // Add objects for joined CcPlayoutHistoryTemplate rows
+            // Add objects for joined CcFiles rows
 
-            $key2 = CcPlayoutHistoryTemplatePeer::getPrimaryKeyHashFromRow($row, $startcol2);
+            $key2 = CcFilesPeer::getPrimaryKeyHashFromRow($row, $startcol2);
             if ($key2 !== null) {
-                $obj2 = CcPlayoutHistoryTemplatePeer::getInstanceFromPool($key2);
+                $obj2 = CcFilesPeer::getInstanceFromPool($key2);
                 if (!$obj2) {
 
-                    $cls = CcPlayoutHistoryTemplatePeer::getOMClass();
+                    $cls = CcFilesPeer::getOMClass();
 
                     $obj2 = new $cls();
                     $obj2->hydrate($row, $startcol2);
-                    CcPlayoutHistoryTemplatePeer::addInstanceToPool($obj2, $key2);
+                    CcFilesPeer::addInstanceToPool($obj2, $key2);
                 } // if obj2 loaded
 
-                // Add the $obj1 (ThirdPartyTrackReferences) to the collection in $obj2 (CcPlayoutHistoryTemplate)
+                // Add the $obj1 (ThirdPartyTrackReferences) to the collection in $obj2 (CcFiles)
                 $obj2->addThirdPartyTrackReferences($obj1);
             } // if joined row not null
 
diff --git a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
index 8602b947b..86f0a174d 100644
--- a/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseThirdPartyTrackReferencesQuery.php
@@ -9,47 +9,43 @@
  * @method ThirdPartyTrackReferencesQuery orderByDbId($order = Criteria::ASC) Order by the id column
  * @method ThirdPartyTrackReferencesQuery orderByDbService($order = Criteria::ASC) Order by the service column
  * @method ThirdPartyTrackReferencesQuery orderByDbForeignId($order = Criteria::ASC) Order by the foreign_id column
- * @method ThirdPartyTrackReferencesQuery orderByDbBrokerTaskId($order = Criteria::ASC) Order by the broker_task_id column
- * @method ThirdPartyTrackReferencesQuery orderByDbBrokerTaskName($order = Criteria::ASC) Order by the broker_task_name column
- * @method ThirdPartyTrackReferencesQuery orderByDbBrokerTaskDispatchTime($order = Criteria::ASC) Order by the broker_task_dispatch_time column
  * @method ThirdPartyTrackReferencesQuery orderByDbFileId($order = Criteria::ASC) Order by the file_id column
+ * @method ThirdPartyTrackReferencesQuery orderByDbUploadTime($order = Criteria::ASC) Order by the upload_time column
  * @method ThirdPartyTrackReferencesQuery orderByDbStatus($order = Criteria::ASC) Order by the status column
  *
  * @method ThirdPartyTrackReferencesQuery groupByDbId() Group by the id column
  * @method ThirdPartyTrackReferencesQuery groupByDbService() Group by the service column
  * @method ThirdPartyTrackReferencesQuery groupByDbForeignId() Group by the foreign_id column
- * @method ThirdPartyTrackReferencesQuery groupByDbBrokerTaskId() Group by the broker_task_id column
- * @method ThirdPartyTrackReferencesQuery groupByDbBrokerTaskName() Group by the broker_task_name column
- * @method ThirdPartyTrackReferencesQuery groupByDbBrokerTaskDispatchTime() Group by the broker_task_dispatch_time column
  * @method ThirdPartyTrackReferencesQuery groupByDbFileId() Group by the file_id column
+ * @method ThirdPartyTrackReferencesQuery groupByDbUploadTime() Group by the upload_time column
  * @method ThirdPartyTrackReferencesQuery groupByDbStatus() Group by the status column
  *
  * @method ThirdPartyTrackReferencesQuery leftJoin($relation) Adds a LEFT JOIN clause to the query
  * @method ThirdPartyTrackReferencesQuery rightJoin($relation) Adds a RIGHT JOIN clause to the query
  * @method ThirdPartyTrackReferencesQuery innerJoin($relation) Adds a INNER JOIN clause to the query
  *
- * @method ThirdPartyTrackReferencesQuery leftJoinCcPlayoutHistoryTemplate($relationAlias = null) Adds a LEFT JOIN clause to the query using the CcPlayoutHistoryTemplate relation
- * @method ThirdPartyTrackReferencesQuery rightJoinCcPlayoutHistoryTemplate($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CcPlayoutHistoryTemplate relation
- * @method ThirdPartyTrackReferencesQuery innerJoinCcPlayoutHistoryTemplate($relationAlias = null) Adds a INNER JOIN clause to the query using the CcPlayoutHistoryTemplate relation
+ * @method ThirdPartyTrackReferencesQuery leftJoinCcFiles($relationAlias = null) Adds a LEFT JOIN clause to the query using the CcFiles relation
+ * @method ThirdPartyTrackReferencesQuery rightJoinCcFiles($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CcFiles relation
+ * @method ThirdPartyTrackReferencesQuery innerJoinCcFiles($relationAlias = null) Adds a INNER JOIN clause to the query using the CcFiles relation
+ *
+ * @method ThirdPartyTrackReferencesQuery leftJoinCeleryTasks($relationAlias = null) Adds a LEFT JOIN clause to the query using the CeleryTasks relation
+ * @method ThirdPartyTrackReferencesQuery rightJoinCeleryTasks($relationAlias = null) Adds a RIGHT JOIN clause to the query using the CeleryTasks relation
+ * @method ThirdPartyTrackReferencesQuery innerJoinCeleryTasks($relationAlias = null) Adds a INNER JOIN clause to the query using the CeleryTasks relation
  *
  * @method ThirdPartyTrackReferences findOne(PropelPDO $con = null) Return the first ThirdPartyTrackReferences matching the query
  * @method ThirdPartyTrackReferences findOneOrCreate(PropelPDO $con = null) Return the first ThirdPartyTrackReferences matching the query, or a new ThirdPartyTrackReferences object populated from the query conditions when no match is found
  *
  * @method ThirdPartyTrackReferences findOneByDbService(string $service) Return the first ThirdPartyTrackReferences filtered by the service column
  * @method ThirdPartyTrackReferences findOneByDbForeignId(string $foreign_id) Return the first ThirdPartyTrackReferences filtered by the foreign_id column
- * @method ThirdPartyTrackReferences findOneByDbBrokerTaskId(string $broker_task_id) Return the first ThirdPartyTrackReferences filtered by the broker_task_id column
- * @method ThirdPartyTrackReferences findOneByDbBrokerTaskName(string $broker_task_name) Return the first ThirdPartyTrackReferences filtered by the broker_task_name column
- * @method ThirdPartyTrackReferences findOneByDbBrokerTaskDispatchTime(string $broker_task_dispatch_time) Return the first ThirdPartyTrackReferences filtered by the broker_task_dispatch_time column
  * @method ThirdPartyTrackReferences findOneByDbFileId(int $file_id) Return the first ThirdPartyTrackReferences filtered by the file_id column
+ * @method ThirdPartyTrackReferences findOneByDbUploadTime(string $upload_time) Return the first ThirdPartyTrackReferences filtered by the upload_time column
  * @method ThirdPartyTrackReferences findOneByDbStatus(string $status) Return the first ThirdPartyTrackReferences filtered by the status column
  *
  * @method array findByDbId(int $id) Return ThirdPartyTrackReferences objects filtered by the id column
  * @method array findByDbService(string $service) Return ThirdPartyTrackReferences objects filtered by the service column
  * @method array findByDbForeignId(string $foreign_id) Return ThirdPartyTrackReferences objects filtered by the foreign_id column
- * @method array findByDbBrokerTaskId(string $broker_task_id) Return ThirdPartyTrackReferences objects filtered by the broker_task_id column
- * @method array findByDbBrokerTaskName(string $broker_task_name) Return ThirdPartyTrackReferences objects filtered by the broker_task_name column
- * @method array findByDbBrokerTaskDispatchTime(string $broker_task_dispatch_time) Return ThirdPartyTrackReferences objects filtered by the broker_task_dispatch_time column
  * @method array findByDbFileId(int $file_id) Return ThirdPartyTrackReferences objects filtered by the file_id column
+ * @method array findByDbUploadTime(string $upload_time) Return ThirdPartyTrackReferences objects filtered by the upload_time column
  * @method array findByDbStatus(string $status) Return ThirdPartyTrackReferences objects filtered by the status column
  *
  * @package    propel.generator.airtime.om
@@ -158,7 +154,7 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
      */
     protected function findPkSimple($key, $con)
     {
-        $sql = 'SELECT "id", "service", "foreign_id", "broker_task_id", "broker_task_name", "broker_task_dispatch_time", "file_id", "status" FROM "third_party_track_references" WHERE "id" = :p0';
+        $sql = 'SELECT "id", "service", "foreign_id", "file_id", "upload_time", "status" FROM "third_party_track_references" WHERE "id" = :p0';
         try {
             $stmt = $con->prepare($sql);
             $stmt->bindValue(':p0', $key, PDO::PARAM_INT);
@@ -347,107 +343,6 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
         return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FOREIGN_ID, $dbForeignId, $comparison);
     }
 
-    /**
-     * Filter the query on the broker_task_id column
-     *
-     * Example usage:
-     * <code>
-     * $query->filterByDbBrokerTaskId('fooValue');   // WHERE broker_task_id = 'fooValue'
-     * $query->filterByDbBrokerTaskId('%fooValue%'); // WHERE broker_task_id LIKE '%fooValue%'
-     * </code>
-     *
-     * @param     string $dbBrokerTaskId The value to use as filter.
-     *              Accepts wildcards (* and % trigger a LIKE)
-     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
-     *
-     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
-     */
-    public function filterByDbBrokerTaskId($dbBrokerTaskId = null, $comparison = null)
-    {
-        if (null === $comparison) {
-            if (is_array($dbBrokerTaskId)) {
-                $comparison = Criteria::IN;
-            } elseif (preg_match('/[\%\*]/', $dbBrokerTaskId)) {
-                $dbBrokerTaskId = str_replace('*', '%', $dbBrokerTaskId);
-                $comparison = Criteria::LIKE;
-            }
-        }
-
-        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_ID, $dbBrokerTaskId, $comparison);
-    }
-
-    /**
-     * Filter the query on the broker_task_name column
-     *
-     * Example usage:
-     * <code>
-     * $query->filterByDbBrokerTaskName('fooValue');   // WHERE broker_task_name = 'fooValue'
-     * $query->filterByDbBrokerTaskName('%fooValue%'); // WHERE broker_task_name LIKE '%fooValue%'
-     * </code>
-     *
-     * @param     string $dbBrokerTaskName The value to use as filter.
-     *              Accepts wildcards (* and % trigger a LIKE)
-     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
-     *
-     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
-     */
-    public function filterByDbBrokerTaskName($dbBrokerTaskName = null, $comparison = null)
-    {
-        if (null === $comparison) {
-            if (is_array($dbBrokerTaskName)) {
-                $comparison = Criteria::IN;
-            } elseif (preg_match('/[\%\*]/', $dbBrokerTaskName)) {
-                $dbBrokerTaskName = str_replace('*', '%', $dbBrokerTaskName);
-                $comparison = Criteria::LIKE;
-            }
-        }
-
-        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_NAME, $dbBrokerTaskName, $comparison);
-    }
-
-    /**
-     * Filter the query on the broker_task_dispatch_time column
-     *
-     * Example usage:
-     * <code>
-     * $query->filterByDbBrokerTaskDispatchTime('2011-03-14'); // WHERE broker_task_dispatch_time = '2011-03-14'
-     * $query->filterByDbBrokerTaskDispatchTime('now'); // WHERE broker_task_dispatch_time = '2011-03-14'
-     * $query->filterByDbBrokerTaskDispatchTime(array('max' => 'yesterday')); // WHERE broker_task_dispatch_time < '2011-03-13'
-     * </code>
-     *
-     * @param     mixed $dbBrokerTaskDispatchTime The value to use as filter.
-     *              Values can be integers (unix timestamps), DateTime objects, or strings.
-     *              Empty strings are treated as NULL.
-     *              Use scalar values for equality.
-     *              Use array values for in_array() equivalent.
-     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
-     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
-     *
-     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
-     */
-    public function filterByDbBrokerTaskDispatchTime($dbBrokerTaskDispatchTime = null, $comparison = null)
-    {
-        if (is_array($dbBrokerTaskDispatchTime)) {
-            $useMinMax = false;
-            if (isset($dbBrokerTaskDispatchTime['min'])) {
-                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $dbBrokerTaskDispatchTime['min'], Criteria::GREATER_EQUAL);
-                $useMinMax = true;
-            }
-            if (isset($dbBrokerTaskDispatchTime['max'])) {
-                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $dbBrokerTaskDispatchTime['max'], Criteria::LESS_EQUAL);
-                $useMinMax = true;
-            }
-            if ($useMinMax) {
-                return $this;
-            }
-            if (null === $comparison) {
-                $comparison = Criteria::IN;
-            }
-        }
-
-        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::BROKER_TASK_DISPATCH_TIME, $dbBrokerTaskDispatchTime, $comparison);
-    }
-
     /**
      * Filter the query on the file_id column
      *
@@ -459,7 +354,7 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
      * $query->filterByDbFileId(array('max' => 12)); // WHERE file_id <= 12
      * </code>
      *
-     * @see       filterByCcPlayoutHistoryTemplate()
+     * @see       filterByCcFiles()
      *
      * @param     mixed $dbFileId The value to use as filter.
      *              Use scalar values for equality.
@@ -492,6 +387,49 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
         return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $dbFileId, $comparison);
     }
 
+    /**
+     * Filter the query on the upload_time column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbUploadTime('2011-03-14'); // WHERE upload_time = '2011-03-14'
+     * $query->filterByDbUploadTime('now'); // WHERE upload_time = '2011-03-14'
+     * $query->filterByDbUploadTime(array('max' => 'yesterday')); // WHERE upload_time < '2011-03-13'
+     * </code>
+     *
+     * @param     mixed $dbUploadTime The value to use as filter.
+     *              Values can be integers (unix timestamps), DateTime objects, or strings.
+     *              Empty strings are treated as NULL.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function filterByDbUploadTime($dbUploadTime = null, $comparison = null)
+    {
+        if (is_array($dbUploadTime)) {
+            $useMinMax = false;
+            if (isset($dbUploadTime['min'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::UPLOAD_TIME, $dbUploadTime['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbUploadTime['max'])) {
+                $this->addUsingAlias(ThirdPartyTrackReferencesPeer::UPLOAD_TIME, $dbUploadTime['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
+                $comparison = Criteria::IN;
+            }
+        }
+
+        return $this->addUsingAlias(ThirdPartyTrackReferencesPeer::UPLOAD_TIME, $dbUploadTime, $comparison);
+    }
+
     /**
      * Filter the query on the status column
      *
@@ -522,43 +460,43 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
     }
 
     /**
-     * Filter the query by a related CcPlayoutHistoryTemplate object
+     * Filter the query by a related CcFiles object
      *
-     * @param   CcPlayoutHistoryTemplate|PropelObjectCollection $ccPlayoutHistoryTemplate The related object(s) to use as filter
+     * @param   CcFiles|PropelObjectCollection $ccFiles The related object(s) to use as filter
      * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
      *
      * @return                 ThirdPartyTrackReferencesQuery The current query, for fluid interface
      * @throws PropelException - if the provided filter is invalid.
      */
-    public function filterByCcPlayoutHistoryTemplate($ccPlayoutHistoryTemplate, $comparison = null)
+    public function filterByCcFiles($ccFiles, $comparison = null)
     {
-        if ($ccPlayoutHistoryTemplate instanceof CcPlayoutHistoryTemplate) {
+        if ($ccFiles instanceof CcFiles) {
             return $this
-                ->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $ccPlayoutHistoryTemplate->getDbId(), $comparison);
-        } elseif ($ccPlayoutHistoryTemplate instanceof PropelObjectCollection) {
+                ->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $ccFiles->getDbId(), $comparison);
+        } elseif ($ccFiles instanceof PropelObjectCollection) {
             if (null === $comparison) {
                 $comparison = Criteria::IN;
             }
 
             return $this
-                ->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $ccPlayoutHistoryTemplate->toKeyValue('PrimaryKey', 'DbId'), $comparison);
+                ->addUsingAlias(ThirdPartyTrackReferencesPeer::FILE_ID, $ccFiles->toKeyValue('PrimaryKey', 'DbId'), $comparison);
         } else {
-            throw new PropelException('filterByCcPlayoutHistoryTemplate() only accepts arguments of type CcPlayoutHistoryTemplate or PropelCollection');
+            throw new PropelException('filterByCcFiles() only accepts arguments of type CcFiles or PropelCollection');
         }
     }
 
     /**
-     * Adds a JOIN clause to the query using the CcPlayoutHistoryTemplate relation
+     * Adds a JOIN clause to the query using the CcFiles relation
      *
      * @param     string $relationAlias optional alias for the relation
      * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
      *
      * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
      */
-    public function joinCcPlayoutHistoryTemplate($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    public function joinCcFiles($relationAlias = null, $joinType = Criteria::INNER_JOIN)
     {
         $tableMap = $this->getTableMap();
-        $relationMap = $tableMap->getRelation('CcPlayoutHistoryTemplate');
+        $relationMap = $tableMap->getRelation('CcFiles');
 
         // create a ModelJoin object for this join
         $join = new ModelJoin();
@@ -573,14 +511,14 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
             $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
             $this->addJoinObject($join, $relationAlias);
         } else {
-            $this->addJoinObject($join, 'CcPlayoutHistoryTemplate');
+            $this->addJoinObject($join, 'CcFiles');
         }
 
         return $this;
     }
 
     /**
-     * Use the CcPlayoutHistoryTemplate relation CcPlayoutHistoryTemplate object
+     * Use the CcFiles relation CcFiles object
      *
      * @see       useQuery()
      *
@@ -588,13 +526,87 @@ abstract class BaseThirdPartyTrackReferencesQuery extends ModelCriteria
      *                                   to be used as main alias in the secondary query
      * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
      *
-     * @return   CcPlayoutHistoryTemplateQuery A secondary query class using the current class as primary query
+     * @return   CcFilesQuery A secondary query class using the current class as primary query
      */
-    public function useCcPlayoutHistoryTemplateQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    public function useCcFilesQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
     {
         return $this
-            ->joinCcPlayoutHistoryTemplate($relationAlias, $joinType)
-            ->useQuery($relationAlias ? $relationAlias : 'CcPlayoutHistoryTemplate', 'CcPlayoutHistoryTemplateQuery');
+            ->joinCcFiles($relationAlias, $joinType)
+            ->useQuery($relationAlias ? $relationAlias : 'CcFiles', 'CcFilesQuery');
+    }
+
+    /**
+     * Filter the query by a related CeleryTasks object
+     *
+     * @param   CeleryTasks|PropelObjectCollection $celeryTasks  the related object to use as filter
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return                 ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     * @throws PropelException - if the provided filter is invalid.
+     */
+    public function filterByCeleryTasks($celeryTasks, $comparison = null)
+    {
+        if ($celeryTasks instanceof CeleryTasks) {
+            return $this
+                ->addUsingAlias(ThirdPartyTrackReferencesPeer::ID, $celeryTasks->getDbTrackReference(), $comparison);
+        } elseif ($celeryTasks instanceof PropelObjectCollection) {
+            return $this
+                ->useCeleryTasksQuery()
+                ->filterByPrimaryKeys($celeryTasks->getPrimaryKeys())
+                ->endUse();
+        } else {
+            throw new PropelException('filterByCeleryTasks() only accepts arguments of type CeleryTasks or PropelCollection');
+        }
+    }
+
+    /**
+     * Adds a JOIN clause to the query using the CeleryTasks relation
+     *
+     * @param     string $relationAlias optional alias for the relation
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return ThirdPartyTrackReferencesQuery The current query, for fluid interface
+     */
+    public function joinCeleryTasks($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        $tableMap = $this->getTableMap();
+        $relationMap = $tableMap->getRelation('CeleryTasks');
+
+        // create a ModelJoin object for this join
+        $join = new ModelJoin();
+        $join->setJoinType($joinType);
+        $join->setRelationMap($relationMap, $this->useAliasInSQL ? $this->getModelAlias() : null, $relationAlias);
+        if ($previousJoin = $this->getPreviousJoin()) {
+            $join->setPreviousJoin($previousJoin);
+        }
+
+        // add the ModelJoin to the current object
+        if ($relationAlias) {
+            $this->addAlias($relationAlias, $relationMap->getRightTable()->getName());
+            $this->addJoinObject($join, $relationAlias);
+        } else {
+            $this->addJoinObject($join, 'CeleryTasks');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Use the CeleryTasks relation CeleryTasks object
+     *
+     * @see       useQuery()
+     *
+     * @param     string $relationAlias optional alias for the relation,
+     *                                   to be used as main alias in the secondary query
+     * @param     string $joinType Accepted values are null, 'left join', 'right join', 'inner join'
+     *
+     * @return   CeleryTasksQuery A secondary query class using the current class as primary query
+     */
+    public function useCeleryTasksQuery($relationAlias = null, $joinType = Criteria::INNER_JOIN)
+    {
+        return $this
+            ->joinCeleryTasks($relationAlias, $joinType)
+            ->useQuery($relationAlias ? $relationAlias : 'CeleryTasks', 'CeleryTasksQuery');
     }
 
     /**
diff --git a/airtime_mvc/application/services/CeleryService.php b/airtime_mvc/application/services/CeleryService.php
new file mode 100644
index 000000000..f47ccd715
--- /dev/null
+++ b/airtime_mvc/application/services/CeleryService.php
@@ -0,0 +1,206 @@
+<?php
+
+require_once "CeleryServiceFactory.php";
+
+class CeleryService {
+
+    /**
+     * @var int milliseconds (for compatibility with celery) until we consider a message to have timed out
+     */
+    private static $_CELERY_MESSAGE_TIMEOUT = 600000;  // 10 minutes
+
+    /**
+     * We have to use celeryresults (the default results exchange) because php-celery
+     * doesn't support named results exchanges.
+     *
+     * @var string exchange for celery task results
+     */
+    private static $_CELERY_RESULTS_EXCHANGE = 'celeryresults';
+
+    /**
+     * Connect to the Celery daemon via amqp
+     *
+     * @param $config   array  the airtime configuration array
+     * @param $exchange string the amqp exchange name
+     * @param $queue    string the amqp queue name
+     *
+     * @return Celery the Celery connection object
+     *
+     * @throws Exception when a connection error occurs
+     */
+    private static function _setupCeleryExchange($config, $exchange, $queue) {
+        return new Celery($config["rabbitmq"]["host"],
+                          $config["rabbitmq"]["user"],
+                          $config["rabbitmq"]["password"],
+                          $config["rabbitmq"]["vhost"],
+                          $exchange,                        // Exchange name
+                          $queue,                           // Binding/queue
+                          $config["rabbitmq"]["port"],
+                          false,
+                          true,                             // Persistent messages
+                          self::$_CELERY_MESSAGE_TIMEOUT);  // Result expiration
+    }
+
+    /**
+     * Send an amqp message to Celery the airtime-celery daemon to perform a task
+     *
+     * @param $task     string the Celery task name
+     * @param $exchange string the amqp exchange name
+     * @param $data     array  an associative array containing arguments for the Celery task
+     *
+     * @return string the task identifier for the started Celery task so we can fetch the
+     *                results asynchronously later
+     *
+     * @throws CeleryException when no message is found
+     */
+    public static function sendCeleryMessage($task, $exchange, $data) {
+        $config = parse_ini_file(Application_Model_RabbitMq::getRmqConfigPath(), true);
+        $queue = $routingKey = $exchange;
+        $c = self::_setupCeleryExchange($config, $exchange, $queue);  // Use the exchange name for the queue
+        $result = $c->PostTask($task, $data, true, $routingKey);      // and routing key
+        return $result->getId();
+    }
+
+    /**
+     * Given a task name and identifier, check the Celery results queue for any
+     * corresponding messages
+     *
+     * @param $task CeleryTasks the Celery task object
+     *
+     * @return object the message object
+     *
+     * @throws CeleryTimeoutException when no message is found and more than
+     *                                $_CELERY_MESSAGE_TIMEOUT milliseconds have passed
+     */
+    private static function getAsyncResultMessage($task) {
+        $config = parse_ini_file(Application_Model_RabbitMq::getRmqConfigPath(), true);
+        $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $task;
+        $c = self::_setupCeleryExchange($config, self::$_CELERY_RESULTS_EXCHANGE, $queue);
+        $message = $c->getAsyncResultMessage($task->getDbName(), $task->getDbId());
+
+        // If the message isn't ready yet (Celery hasn't finished the task),
+        // only throw an exception if the message has timed out.
+        if ($message == FALSE && self::_checkMessageTimeout($task)) {
+            throw new CeleryTimeoutException("Celery task " . $task->getDbName()
+                                             . " with ID " . $task->getDbId() . " timed out");
+        }
+        return $message;
+    }
+
+    /**
+     * Check to see if there are any pending tasks for this service
+     *
+     * @param string $taskName    the name of the task to poll for
+     * @param string $serviceName the name of the service to poll for
+     *
+     * @return bool true if there are any pending tasks, otherwise false
+     */
+    public static function isBrokerTaskQueueEmpty($taskName="", $serviceName = "") {
+        $pendingTasks = self::_getPendingTasks($taskName, $serviceName);
+        return empty($pendingTasks);
+    }
+
+    /**
+     * Poll the message queue for this service to see if any tasks with the given name have completed
+     *
+     * If we find any completed tasks, adjust the ThirdPartyTrackReferences table accordingly
+     *
+     * If no task name is passed, we poll all tasks for this service
+     *
+     * @param string $taskName    the name of the task to poll for
+     * @param string $serviceName the name of the service to poll for
+     */
+    public static function pollBrokerTaskQueue($taskName = "", $serviceName = "") {
+        $pendingTasks = self::_getPendingTasks($taskName, $serviceName);
+        foreach ($pendingTasks as $task) {
+            try {
+                $message = self::_getTaskMessage($task);
+                self::_processTaskMessage($task, $message);
+            } catch (CeleryTimeoutException $e) {
+                // If the task times out, mark it as failed. We don't want to remove the
+                // track reference here in case it was a deletion that failed, for example.
+                // TODO: Move this somewhere more appropriate
+                $task->setDbStatus(CELERY_FAILED_STATUS);
+                $task->save();
+                Logging::info($e->getMessage());
+            } catch (Exception $e) {
+                // Because $message->result can be either an object or a string, sometimes
+                // we get a json_decode error and end up here
+                Logging::info($e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * Return a collection of all pending CeleryTasks for this service or task
+     *
+     * @param string $taskName    the name of the task to find
+     * @param string $serviceName the name of the service to find
+     *
+     * @return PropelCollection any pending CeleryTasks results for this service
+     *                          or task if taskName is provided
+     */
+    protected static function _getPendingTasks($taskName, $serviceName) {
+        $query = CeleryTasksQuery::create()
+            ->filterByDbStatus(CELERY_PENDING_STATUS)
+            ->filterByDbId('', Criteria::NOT_EQUAL);
+        if (!empty($taskName)) {
+            $query->filterByDbName($taskName);
+        }
+        if (!empty($serviceName)) {
+            $query->useThirdPartyTrackReferencesQuery()
+                ->filterByDbService($serviceName)->endUse();
+        }
+        return $query->joinThirdPartyTrackReferences()
+            ->with('ThirdPartyTrackReferences')->find();
+    }
+
+    /**
+     * Get a Celery task message from the results queue
+     *
+     * @param $task CeleryTasks the Celery task object
+     *
+     * @return object the task message object
+     *
+     * @throws CeleryException when the result message for this task no longer exists
+     */
+    protected static function _getTaskMessage($task) {
+        $message =  self::getAsyncResultMessage($task);
+        return json_decode($message['body']);
+    }
+
+    /**
+     * Process a message from the results queue
+     *
+     * @param $task    CeleryTasks  Celery task object
+     * @param $message mixed        async message object from php-celery
+     */
+    protected static function _processTaskMessage($task, $message) {
+        $ref = $task->getThirdPartyTrackReferences();  // ThirdPartyTrackReferences join
+        $service = CeleryServiceFactory::getService($ref->getDbService());
+        if ($message->status == CELERY_SUCCESS_STATUS
+            && $task->getDbName() == $service->getCeleryDeleteTaskName()) {
+            $service->removeTrackReference($ref->getDbFileId());
+        } else {
+            $service->updateTrackReference($ref->getDbId(), json_decode($message->result), $message->status);
+        }
+    }
+
+    /**
+     * Check if a task message has been unreachable for more our timeout time
+     *
+     * @param $task CeleryTasks the Celery task object
+     *
+     * @return bool true if the dispatch time is empty or it's been more than our timeout time
+     *              since the message was dispatched, otherwise false
+     */
+    protected static function _checkMessageTimeout($task) {
+        $utc = new DateTimeZone("UTC");
+        $dispatchTime = new DateTime($task->getDbDispatchTime(), $utc);
+        $now = new DateTime("now", $utc);
+        $timeoutSeconds = self::$_CELERY_MESSAGE_TIMEOUT / 1000;  // Convert from milliseconds
+        $timeoutInterval = new DateInterval("PT" . $timeoutSeconds . "S");
+        return (empty($dispatchTime) || $dispatchTime->add($timeoutInterval) <= $now);
+    }
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/services/CeleryServiceFactory.php b/airtime_mvc/application/services/CeleryServiceFactory.php
new file mode 100644
index 000000000..defb6ce81
--- /dev/null
+++ b/airtime_mvc/application/services/CeleryServiceFactory.php
@@ -0,0 +1,20 @@
+<?php
+
+class CeleryServiceFactory {
+
+    /**
+     *
+     *
+     * @param $serviceName string the name of the service to create
+     *
+     * @return ThirdPartyCeleryService|null
+     */
+    public static function getService($serviceName) {
+        switch($serviceName) {
+            case SOUNDCLOUD_SERVICE_NAME:
+                return new SoundcloudService();
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index 1745d29a8..5ae0a734b 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -1,8 +1,8 @@
 <?php
 
-require_once "ThirdPartyService.php";
+require_once "ThirdPartyCeleryService.php";
 
-class SoundcloudService extends ThirdPartyService {
+class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
 
     /**
      * @var string service access token for accessing remote API
@@ -17,7 +17,7 @@ class SoundcloudService extends ThirdPartyService {
     /**
      * @var string service name to store in ThirdPartyTrackReferences database
      */
-    protected static $_SERVICE_NAME = 'SoundCloud';
+    protected static $_SERVICE_NAME = SOUNDCLOUD_SERVICE_NAME;  // SoundCloud service name constant from constants.php
 
     /**
      * @var string exchange name for SoundCloud tasks
@@ -84,25 +84,20 @@ class SoundcloudService extends ThirdPartyService {
     /**
      * Update a ThirdPartyTrackReferences object for a completed upload
      * TODO: should we have a database layer class to handle Propel operations?
-     * TODO: break this function up, it's a bit of a beast
      *
-     * @param $fileId int    local CcFiles identifier
-     * @param $track  object third-party service track object
-     * @param $status string Celery task status
+     * @param $trackId int    ThirdPartyTrackReferences identifier
+     * @param $track  object  third-party service track object
+     * @param $status string  Celery task status
      *
      * @throws Exception
      * @throws PropelException
      */
-    protected function _addOrUpdateTrackReference($fileId, $track, $status) {
+    public function updateTrackReference($trackId, $track, $status) {
+        parent::updateTrackReference($trackId, $track, $status);
         $ref = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService(static::$_SERVICE_NAME)
-            ->findOneByDbFileId($fileId);
+            ->findOneByDbId($trackId);
         if (is_null($ref)) {
             $ref = new ThirdPartyTrackReferences();
-        }  // If this was a delete task, just remove the record and return
-        else if ($ref->getDbBrokerTaskName() == static::$_CELERY_DELETE_TASK_NAME) {
-            $ref->delete();
-            return;
         }
         $ref->setDbService(static::$_SERVICE_NAME);
         // Only set the SoundCloud fields if the task was successful
@@ -110,15 +105,8 @@ class SoundcloudService extends ThirdPartyService {
             // TODO: fetch any additional SoundCloud parameters we want to store
             $ref->setDbForeignId($track->id);  // SoundCloud identifier
         }
-        $ref->setDbFileId($fileId);
-        $ref->setDbStatus($status);
-        // Null the broker task fields because we no longer need them
-        // We use NULL over an empty string/object here because we have
-        // a unique constraint on the task ID and it's easier to filter
-        // and query against NULLs
-        $ref->setDbBrokerTaskId(NULL);
-        $ref->setDbBrokerTaskName(NULL);
-        $ref->setDbBrokerTaskDispatchTime(NULL);
+        // TODO: set SoundCloud upload status?
+        // $ref->setDbStatus($status);
         $ref->save();
     }
 
diff --git a/airtime_mvc/application/services/ThirdPartyCeleryService.php b/airtime_mvc/application/services/ThirdPartyCeleryService.php
new file mode 100644
index 000000000..5588c2cf7
--- /dev/null
+++ b/airtime_mvc/application/services/ThirdPartyCeleryService.php
@@ -0,0 +1,135 @@
+<?php
+
+require_once "ThirdPartyService.php";
+
+abstract class ThirdPartyCeleryService extends ThirdPartyService {
+
+    /**
+     * @var string broker exchange name for third-party tasks
+     */
+    protected static $_CELERY_EXCHANGE_NAME;
+
+    /**
+     * @var string celery task name for third-party uploads
+     */
+    protected static $_CELERY_UPLOAD_TASK_NAME;
+
+    /**
+     * @var string celery task name for third-party deletion
+     */
+    protected static $_CELERY_DELETE_TASK_NAME;
+
+    /**
+     * Upload the file with the given identifier to a third-party service
+     *
+     * @param int $fileId the local CcFiles identifier
+     */
+    public function upload($fileId) {
+        $file = Application_Model_StoredFile::RecallById($fileId);
+        $data = array(
+            'data' => $this->_getUploadData($file),
+            'token' => $this->_accessToken,
+            'file_path' => $file->getFilePaths()[0]
+        );
+        try {
+            $brokerTaskId = CeleryService::sendCeleryMessage(static::$_CELERY_UPLOAD_TASK_NAME,
+                                                             static::$_CELERY_EXCHANGE_NAME,
+                                                             $data);
+            $this->_createTaskReference($fileId, $brokerTaskId, static::$_CELERY_UPLOAD_TASK_NAME);
+        } catch (Exception $e) {
+            Logging::info("Invalid request: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * Delete the file with the given identifier from a third-party service
+     *
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @throws ServiceNotFoundException when a $fileId with no corresponding
+     *                                  service identifier is given
+     */
+    public function delete($fileId) {
+        $serviceId = $this->getServiceId($fileId);
+        if ($serviceId == 0) {
+            throw new ServiceNotFoundException("No service found for file with ID $fileId");
+        }
+        $data = array(
+            'token' => $this->_accessToken,
+            'track_id' => $serviceId
+        );
+        try {
+            $brokerTaskId = CeleryService::sendCeleryMessage(static::$_CELERY_DELETE_TASK_NAME,
+                                                             static::$_CELERY_EXCHANGE_NAME,
+                                                             $data);
+            $this->_createTaskReference($fileId, $brokerTaskId, static::$_CELERY_DELETE_TASK_NAME);
+        } catch (Exception $e) {
+            Logging::info("Invalid request: " . $e->getMessage());
+        }
+    }
+
+    /**
+     * Create a CeleryTasks object for a pending task
+     * TODO: should we have a database layer class to handle Propel operations?
+     *
+     * @param $fileId       int    CcFiles identifier
+     * @param $brokerTaskId int    broker task identifier to so we can asynchronously
+     *                             receive completed task messages
+     * @param $taskName     string broker task name
+     *
+     * @throws Exception
+     * @throws PropelException
+     */
+    protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
+        $trackId = $this->createTrackReference($fileId);
+        // First, check if the track already has an entry in the database
+        $ref = CeleryTasksQuery::create()->findOneByDbId($brokerTaskId);
+        if (is_null($ref)) {
+            $ref = new CeleryTasks();
+        }
+        $ref->setDbId($brokerTaskId);
+        $ref->setDbName($taskName);
+        $utc = new DateTimeZone("UTC");
+        $ref->setDbDispatchTime(new DateTime("now", $utc));
+        $ref->setDbStatus(CELERY_PENDING_STATUS);
+        $ref->setDbTrackReference($trackId);
+        $ref->save();
+    }
+
+    /**
+     * Update a CeleryTasks object for a completed upload
+     * TODO: should we have a database layer class to handle Propel operations?
+     *
+     * @param $trackId int    ThirdPartyTrackReferences identifier
+     * @param $track  object  third-party service track object
+     * @param $status string  Celery task status
+     *
+     * @throws Exception
+     * @throws PropelException
+     */
+    public function updateTrackReference($trackId, $track, $status) {
+        $ref = CeleryTasksQuery::create()
+            ->findOneByDbTrackReference($trackId);
+        $ref->setDbStatus($status);
+        $ref->save();
+    }
+
+    /**
+     * Build a parameter array for the file being uploaded to a third party service
+     *
+     * @param $file Application_Model_StoredFile the file being uploaded
+     *
+     * @return array the track array to send to the third party service
+     */
+    abstract protected function _getUploadData($file);
+
+    /**
+     * Field accessor for $_CELERY_DELETE_TASK_NAME
+     *
+     * @return string the Celery task name for deleting tracks from this service
+     */
+    public function getCeleryDeleteTaskName() {
+        return self::$_CELERY_DELETE_TASK_NAME;
+    }
+
+}
\ No newline at end of file
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
index 058f3db97..365002ee7 100644
--- a/airtime_mvc/application/services/ThirdPartyService.php
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -7,12 +7,11 @@ class ServiceNotFoundException extends Exception {}
 
 /**
  * Class ThirdPartyService generic superclass for third-party services
- * TODO: decouple the media/track-specific functions into ThirdPartyMediaService class?
  */
 abstract class ThirdPartyService {
 
     /**
-     * @var string service access token for accessing remote API
+     * @var string service access token for accessing third-party API
      */
     protected $_accessToken;
 
@@ -27,86 +26,18 @@ abstract class ThirdPartyService {
     protected static $_THIRD_PARTY_TRACK_URI;
 
     /**
-     * @var string broker exchange name for third party tasks
-     */
-    protected static $_CELERY_EXCHANGE_NAME;
-
-    /**
-     * @var string celery task name for third party uploads
-     */
-    protected static $_CELERY_UPLOAD_TASK_NAME;
-
-    /**
-     * @var string celery task name for third party deletion
-     */
-    protected static $_CELERY_DELETE_TASK_NAME;
-
-    /**
-     * Upload the file with the given identifier to a third-party service
-     *
-     * @param int $fileId the local CcFiles identifier
-     */
-    public function upload($fileId) {
-        $file = Application_Model_StoredFile::RecallById($fileId);
-        $data = array(
-            'data' => $this->_getUploadData($file),
-            'token' => $this->_accessToken,
-            'file_path' => $file->getFilePaths()[0]
-        );
-        try {
-            $brokerTaskId = Application_Model_RabbitMq::sendCeleryMessage(static::$_CELERY_UPLOAD_TASK_NAME,
-                                                                          static::$_CELERY_EXCHANGE_NAME,
-                                                                          $data);
-            $this->_createTaskReference($fileId, $brokerTaskId, static::$_CELERY_UPLOAD_TASK_NAME);
-        } catch (Exception $e) {
-            Logging::info("Invalid request: " . $e->getMessage());
-            // We should only get here if we have an access token, so attempt to refresh
-            $this->accessTokenRefresh();
-        }
-    }
-
-    /**
-     * Delete the file with the given identifier from a third-party service
-     *
-     * @param int $fileId the local CcFiles identifier
-     *
-     * @throws ServiceNotFoundException when a $fileId with no corresponding
-     *                                  service identifier is given
-     */
-    public function delete($fileId) {
-        $serviceId = $this->getServiceId($fileId);
-        if ($serviceId == 0) {
-            throw new ServiceNotFoundException("No service found for file with ID $fileId");
-        }
-        $data = array(
-            'token' => $this->_accessToken,
-            'track_id' => $serviceId
-        );
-        try {
-            $brokerTaskId = Application_Model_RabbitMq::sendCeleryMessage(static::$_CELERY_DELETE_TASK_NAME,
-                                                                          static::$_CELERY_EXCHANGE_NAME,
-                                                                          $data);
-            $this->_createTaskReference($fileId, $brokerTaskId, static::$_CELERY_DELETE_TASK_NAME);
-        } catch (Exception $e) {
-            Logging::info("Invalid request: " . $e->getMessage());
-            // We should only get here if we have an access token, so attempt to refresh
-            $this->accessTokenRefresh();
-        }
-    }
-
-    /**
-     * Create a ThirdPartyTrackReferences object for a pending task
+     * Create a ThirdPartyTrackReferences object for a track that's been uploaded
+     * to an external service
      * TODO: should we have a database layer class to handle Propel operations?
      *
-     * @param $fileId       int    local CcFiles identifier
-     * @param $brokerTaskId int    broker task identifier to so we can asynchronously
-     *                             receive completed task messages
-     * @param $taskName     string broker task name
+     * @param $fileId int local CcFiles identifier
+     *
+     * @return string the new ThirdPartyTrackReferences identifier
      *
      * @throws Exception
      * @throws PropelException
      */
-    protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
+    public function createTrackReference($fileId) {
         // First, check if the track already has an entry in the database
         $ref = ThirdPartyTrackReferencesQuery::create()
             ->filterByDbService(static::$_SERVICE_NAME)
@@ -115,13 +46,11 @@ abstract class ThirdPartyService {
             $ref = new ThirdPartyTrackReferences();
         }
         $ref->setDbService(static::$_SERVICE_NAME);
-        $ref->setDbBrokerTaskId($brokerTaskId);
-        $ref->setDbBrokerTaskName($taskName);
-        $utc = new DateTimeZone("UTC");
-        $ref->setDbBrokerTaskDispatchTime(new DateTime("now", $utc));
+        // TODO: implement service-specific statuses?
+        // $ref->setDbStatus(CELERY_PENDING_STATUS);
         $ref->setDbFileId($fileId);
-        $ref->setDbStatus(CELERY_PENDING_STATUS);
         $ref->save();
+        return $ref->getDbId();
     }
 
     /**
@@ -129,7 +58,7 @@ abstract class ThirdPartyService {
      * This is necessary if the track was removed from the service
      * or the foreign id in our database is incorrect
      *
-     * @param $fileId int local CcFiles identifier
+     * @param $fileId int cc_files identifier
      *
      * @throws Exception
      * @throws PropelException
@@ -147,169 +76,55 @@ abstract class ThirdPartyService {
      *
      * @param int $fileId the local CcFiles identifier
      *
-     * @return int the service foreign identifier
+     * @return string the service foreign identifier
      */
     public function getServiceId($fileId) {
         $ref = ThirdPartyTrackReferencesQuery::create()
             ->filterByDbService(static::$_SERVICE_NAME)
             ->findOneByDbFileId($fileId);  // There shouldn't be duplicates!
-        return empty($ref) ? 0 : $ref->getDbForeignId();
+        return empty($ref) ? '' : $ref->getDbForeignId();
     }
 
     /**
      * Given a CcFiles identifier for a file that's been uploaded to a third-party service,
      * return a link to the remote file
      *
-     * @param int $fileId the local CcFiles identifier
+     * @param int $fileId CcFiles identifier
      *
      * @return string the link to the remote file
      */
     public function getLinkToFile($fileId) {
         $serviceId = $this->getServiceId($fileId);
-        return $serviceId > 0 ? static::$_THIRD_PARTY_TRACK_URI . $serviceId : '';
+        return empty($serviceId) ? '' : static::$_THIRD_PARTY_TRACK_URI . $serviceId;
     }
 
     /**
-     * Check to see if there are any pending tasks for this service
+     * Upload the file with the given identifier to a third-party service
      *
-     * @param string $taskName
-     *
-     * @return bool true if there are any pending tasks, otherwise false
+     * @param int $fileId CcFiles identifier
      */
-    public function isBrokerTaskQueueEmpty($taskName="") {
-        $query = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService(static::$_SERVICE_NAME);
-        if (!empty($taskName)) {
-            $query->filterByDbBrokerTaskName($taskName);
-        }
-        $result = $query->findOneByDbStatus(CELERY_PENDING_STATUS);
-        return empty($result);
-    }
+    abstract function upload($fileId);
 
     /**
-     * Poll the message queue for this service to see if any tasks with the given name have completed
-     * If we find any completed tasks, adjust the ThirdPartyTrackReferences table accordingly
-     * If no task name is passed, we poll all tasks for this service
+     * Delete the file with the given identifier from a third-party service
      *
-     * @param string $taskName the name of the task to poll for
+     * @param int $fileId the local CcFiles identifier
+     *
+     * @throws ServiceNotFoundException when a $fileId with no corresponding
+     *                                  service identifier is given
      */
-    public function pollBrokerTaskQueue($taskName="") {
-        $pendingTasks = static::_getPendingTasks($taskName);
-        foreach ($pendingTasks as $task) {
-            try {
-                $message = static::_getTaskMessage($task);
-                static::_addOrUpdateTrackReference($task->getDbFileId(), json_decode($message->result), $message->status);
-            } catch (CeleryException $e) {
-                // Fail silently unless the message has timed out; often we end up here when
-                // the Celery task takes a while to execute
-                if (static::_checkMessageTimeout($task)) {
-                    Logging::info($e->getMessage());
-                    $task->setDbStatus(CELERY_FAILED_STATUS);
-                    $task->save();
-                }
-            } catch (Exception $e) {
-                // Sometimes we might catch a json_decode error and end up here
-                Logging::info($e->getMessage());
-            }
-        }
-    }
-
-    /**
-     * Return a collection of all pending ThirdPartyTrackReferences to tasks for this service or task
-     *
-     * @param string $taskName the name of the task to look for
-     *
-     * @return PropelCollection any pending ThirdPartyTrackReferences results for this service
-     *                          or task if taskName is provided
-     */
-    protected function _getPendingTasks($taskName) {
-        $query = ThirdPartyTrackReferencesQuery::create()
-            ->filterByDbService(static::$_SERVICE_NAME)
-            ->filterByDbStatus(CELERY_PENDING_STATUS)
-            ->filterByDbBrokerTaskId('', Criteria::NOT_EQUAL);
-        if (!empty($taskName)) {
-            $query->filterByDbBrokerTaskName($taskName);
-        }
-        return $query->find();
-    }
-
-    /**
-     * Get a Celery task message from the results queue
-     *
-     * @param $task ThirdPartyTrackReferences the track reference object
-     *
-     * @return object the task message object
-     *
-     * @throws CeleryException when the result message for this task no longer exists
-     */
-    protected static function _getTaskMessage($task) {
-        $message =  Application_Model_RabbitMq::getAsyncResultMessage($task->getDbBrokerTaskName(),
-                                                                      $task->getDbBrokerTaskId());
-        return json_decode($message['body']);
-    }
-
-    /**
-     * Check if a task message has been unreachable for more our timeout time
-     *
-     * @param $task ThirdPartyTrackReferences the track reference object
-     *
-     * @return bool true if the dispatch time is empty or it's been more than our timeout time
-     *              since the message was dispatched, otherwise false
-     */
-    protected static function _checkMessageTimeout($task) {
-        $utc = new DateTimeZone("UTC");
-        $dispatchTime = new DateTime($task->getDbBrokerTaskDispatchTime(), $utc);
-        $now = new DateTime("now", $utc);
-        $timeoutSeconds = Application_Model_RabbitMq::$_CELERY_MESSAGE_TIMEOUT / 1000;  // Convert from milliseconds
-        $timeoutInterval = new DateInterval("PT" . $timeoutSeconds . "S");
-        return (empty($dispatchTime) || $dispatchTime->add($timeoutInterval) <= $now);
-    }
-
-    /**
-     * Build a parameter array for the file being uploaded to a third party service
-     *
-     * @param $file Application_Model_StoredFile the file being uploaded
-     *
-     * @return array the track array to send to the third party service
-     */
-    abstract protected function _getUploadData($file);
+    abstract function delete($fileId);
 
     /**
      * Update a ThirdPartyTrackReferences object for a completed task
      *
-     * @param $fileId int    local CcFiles identifier
-     * @param $track  object third-party service track object
-     * @param $status string Celery task status
+     * @param $trackId int    ThirdPartyTrackReferences identifier
+     * @param $track  object  third-party service track object
+     * @param $status string  Celery task status
      *
      * @throws Exception
      * @throws PropelException
      */
-    abstract protected function _addOrUpdateTrackReference($fileId, $track, $status);
-
-    /**
-     * Check whether an OAuth access token exists for the third-party client
-     *
-     * @return bool true if an access token exists, otherwise false
-     */
-    abstract function hasAccessToken();
-
-    /**
-     * Get the OAuth authorization URL
-     *
-     * @return string the authorization URL
-     */
-    abstract function getAuthorizeUrl();
-
-    /**
-     * Request a new OAuth access token from a third-party service and store it in CcPref
-     *
-     * @param $code string exchange authorization code for access token
-     */
-    abstract function requestNewAccessToken($code);
-
-    /**
-     * Regenerate the third-party client's OAuth access token
-     */
-    abstract function accessTokenRefresh();
+    abstract function updateTrackReference($trackId, $track, $status);
 
 }
\ No newline at end of file
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index 53a479dc9..a4ce8193b 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -536,19 +536,28 @@
     <column name="service" phpName="DbService" type="VARCHAR" size="256" required="true" />
     <!-- Make foreign ID a VARCHAR field in case a service uses hashes or other non-integer identifiers -->
     <column name="foreign_id" phpName="DbForeignId" type="VARCHAR" size="256" />
-    <column name="broker_task_id" phpName="DbBrokerTaskId" type="VARCHAR" size="256" />
-    <column name="broker_task_name" phpName="DbBrokerTaskName" type="VARCHAR" size="256" />
-    <column name="broker_task_dispatch_time" phpName="DbBrokerTaskDispatchTime" type="TIMESTAMP" />
     <column name="file_id" phpName="DbFileId" type="INTEGER" required="true" />
-    <column name="status" phpName="DbStatus" type="VARCHAR" size="256" required="true" />
-    <unique name="broker_task_id_unique">
-      <unique-column name="broker_task_id"/>
-    </unique>
+    <column name="upload_time" phpName="DbUploadTime" type="TIMESTAMP" />
+    <column name="status" phpName="DbStatus" type="VARCHAR" size="256" />
     <unique name="foreign_id_unique">
       <unique-column name="foreign_id"/>
     </unique>
-    <foreign-key foreignTable="cc_playout_history_template" name="track_reference_fkey" onDelete="CASCADE">
+    <foreign-key foreignTable="cc_files" name="track_reference_fkey" onDelete="CASCADE">
       <reference local="file_id" foreign="id"/>
     </foreign-key>
   </table>
+
+  <table name="celery_tasks" phpName="CeleryTasks">
+    <column name="id" phpName="DbId" primaryKey="true" type="VARCHAR" size="256" required="true" />
+    <column name="track_reference" phpName="DbTrackReference" type="INTEGER" required="true" />
+    <column name="name" phpName="DbName" type="VARCHAR" size="256" />
+    <column name="dispatch_time" phpName="DbDispatchTime" type="TIMESTAMP" />
+    <column name="status" phpName="DbStatus" type="VARCHAR" size="256" required="true" />
+    <unique name="id_unique">
+      <unique-column name="id"/>
+    </unique>
+    <foreign-key foreignTable="third_party_track_references" name="celery_service_fkey" onDelete="CASCADE">
+      <reference local="track_reference" foreign="id"/>
+    </foreign-key>
+  </table>
 </database>
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index 74a39e597..9868de899 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -681,14 +681,28 @@ CREATE TABLE "third_party_track_references"
     "id" serial NOT NULL,
     "service" VARCHAR(256) NOT NULL,
     "foreign_id" VARCHAR(256),
-    "broker_task_id" VARCHAR(256),
-    "broker_task_name" VARCHAR(256),
-    "broker_task_dispatch_time" TIMESTAMP,
     "file_id" INTEGER NOT NULL,
+    "upload_time" TIMESTAMP,
+    "status" VARCHAR(256),
+    PRIMARY KEY ("id"),
+    CONSTRAINT "foreign_id_unique" UNIQUE ("foreign_id")
+);
+
+-----------------------------------------------------------------------
+-- celery_tasks
+-----------------------------------------------------------------------
+
+DROP TABLE IF EXISTS "celery_tasks" CASCADE;
+
+CREATE TABLE "celery_tasks"
+(
+    "id" VARCHAR(256) NOT NULL,
+    "track_reference" INTEGER NOT NULL,
+    "name" VARCHAR(256),
+    "dispatch_time" TIMESTAMP,
     "status" VARCHAR(256) NOT NULL,
     PRIMARY KEY ("id"),
-    CONSTRAINT "broker_task_id_unique" UNIQUE ("broker_task_id"),
-    CONSTRAINT "foreign_id_unique" UNIQUE ("foreign_id")
+    CONSTRAINT "id_unique" UNIQUE ("id")
 );
 
 ALTER TABLE "cc_files" ADD CONSTRAINT "cc_files_owner_fkey"
@@ -855,5 +869,10 @@ ALTER TABLE "cc_playout_history_template_field" ADD CONSTRAINT "cc_playout_histo
 
 ALTER TABLE "third_party_track_references" ADD CONSTRAINT "track_reference_fkey"
     FOREIGN KEY ("file_id")
-    REFERENCES "cc_playout_history_template" ("id")
+    REFERENCES "cc_files" ("id")
+    ON DELETE CASCADE;
+
+ALTER TABLE "celery_tasks" ADD CONSTRAINT "celery_service_fkey"
+    FOREIGN KEY ("track_reference")
+    REFERENCES "third_party_track_references" ("id")
     ON DELETE CASCADE;

From e033360a4ebd7148378986833a6729ac8c0ae134 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 16 Jun 2015 16:21:31 -0400
Subject: [PATCH 11/39] Fix error when deleting SoundCloud tracks

---
 .../application/services/CeleryService.php     |  9 ++++-----
 .../services/ThirdPartyCeleryService.php       | 18 +++++++++---------
 2 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/airtime_mvc/application/services/CeleryService.php b/airtime_mvc/application/services/CeleryService.php
index f47ccd715..9f21af3e4 100644
--- a/airtime_mvc/application/services/CeleryService.php
+++ b/airtime_mvc/application/services/CeleryService.php
@@ -81,6 +81,10 @@ class CeleryService {
         // If the message isn't ready yet (Celery hasn't finished the task),
         // only throw an exception if the message has timed out.
         if ($message == FALSE && self::_checkMessageTimeout($task)) {
+            // If the task times out, mark it as failed. We don't want to remove the
+            // track reference here in case it was a deletion that failed, for example.
+            $task->setDbStatus(CELERY_FAILED_STATUS);
+            $task->save();
             throw new CeleryTimeoutException("Celery task " . $task->getDbName()
                                              . " with ID " . $task->getDbId() . " timed out");
         }
@@ -117,11 +121,6 @@ class CeleryService {
                 $message = self::_getTaskMessage($task);
                 self::_processTaskMessage($task, $message);
             } catch (CeleryTimeoutException $e) {
-                // If the task times out, mark it as failed. We don't want to remove the
-                // track reference here in case it was a deletion that failed, for example.
-                // TODO: Move this somewhere more appropriate
-                $task->setDbStatus(CELERY_FAILED_STATUS);
-                $task->save();
                 Logging::info($e->getMessage());
             } catch (Exception $e) {
                 // Because $message->result can be either an object or a string, sometimes
diff --git a/airtime_mvc/application/services/ThirdPartyCeleryService.php b/airtime_mvc/application/services/ThirdPartyCeleryService.php
index 5588c2cf7..ec3527545 100644
--- a/airtime_mvc/application/services/ThirdPartyCeleryService.php
+++ b/airtime_mvc/application/services/ThirdPartyCeleryService.php
@@ -114,6 +114,15 @@ abstract class ThirdPartyCeleryService extends ThirdPartyService {
         $ref->save();
     }
 
+    /**
+     * Field accessor for $_CELERY_DELETE_TASK_NAME
+     *
+     * @return string the Celery task name for deleting tracks from this service
+     */
+    public function getCeleryDeleteTaskName() {
+        return static::$_CELERY_DELETE_TASK_NAME;
+    }
+
     /**
      * Build a parameter array for the file being uploaded to a third party service
      *
@@ -123,13 +132,4 @@ abstract class ThirdPartyCeleryService extends ThirdPartyService {
      */
     abstract protected function _getUploadData($file);
 
-    /**
-     * Field accessor for $_CELERY_DELETE_TASK_NAME
-     *
-     * @return string the Celery task name for deleting tracks from this service
-     */
-    public function getCeleryDeleteTaskName() {
-        return self::$_CELERY_DELETE_TASK_NAME;
-    }
-
 }
\ No newline at end of file

From 27bebb916846ed1c3e1ce52469b2b54c1093174b Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 17 Jun 2015 13:09:56 -0400
Subject: [PATCH 12/39] Remove logging statement

---
 airtime_mvc/application/common/TaskManager.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php
index 21b2b073f..e7da5e203 100644
--- a/airtime_mvc/application/common/TaskManager.php
+++ b/airtime_mvc/application/common/TaskManager.php
@@ -64,7 +64,8 @@ final class TaskManager {
         } catch (Exception $e) {
             // We get here if there are simultaneous requests trying to fetch the lock row
             $this->_con->rollBack();
-            Logging::info($e->getMessage());
+            // Logging::info($e->getMessage()); // We actually get here a lot, so it's
+                                                // better to be silent here to avoid log bloat
             return;
         }
         foreach ($this->_taskList as $task) {

From afb02301c1ebe5e4a73e1b0023353cbf6d0032f4 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 17 Jun 2015 17:16:21 -0400
Subject: [PATCH 13/39] SAAS-868 - Fixes for pull request #115 -
 https://github.com/sourcefabric/airtime/pull/115

---
 .../application/common/TaskManager.php        | 27 +++++++++++++++----
 .../forms/SoundCloudPreferences.php           | 24 -----------------
 2 files changed, 22 insertions(+), 29 deletions(-)

diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php
index e7da5e203..cec3a2f56 100644
--- a/airtime_mvc/application/common/TaskManager.php
+++ b/airtime_mvc/application/common/TaskManager.php
@@ -48,17 +48,27 @@ final class TaskManager {
     }
 
     /**
-     * Run all tasks that need to be run
+     * Run all tasks that need to be run.
+     *
+     * To prevent blocking and making too many requests to the database,
+     * we implement a row-level, non-blocking, read-protected lock on a
+     * timestamp that we check each time the application is bootstrapped,
+     * which, assuming enough time has passed, is updated before running
+     * the tasks.
      */
     public function runTasks() {
         // If there is data in auth storage, this could be a user request
         // so we should lock the TaskManager to avoid blocking
-        if ($this->_isUserSessionRequest()) return;
+        if ($this->_isUserSessionRequest()) {
+            return;
+        }
         $this->_con = Propel::getConnection(CcPrefPeer::DATABASE_NAME);
         $this->_con->beginTransaction();
         try {
             $lock = $this->_getLock();
-            if ($lock && microtime(true) < $lock['valstr'] + self::TASK_INTERVAL_SECONDS) return;
+            if ($lock && microtime(true) < $lock['valstr'] + self::TASK_INTERVAL_SECONDS) {
+                return;
+            }
             $this->_updateLock($lock);
             $this->_con->commit();
         } catch (Exception $e) {
@@ -70,14 +80,17 @@ final class TaskManager {
         }
         foreach ($this->_taskList as $task) {
             $task = TaskFactory::getTask($task);
-            if ($task && $task->shouldBeRun()) $task->run();
+            if ($task && $task->shouldBeRun()) {
+                $task->run();
+            }
         }
     }
 
     /**
      * Check if the current session is a user request
      *
-     * @return bool
+     * @return bool true if there is a Zend_Auth object in the current session,
+     *              otherwise false
      */
     private function _isUserSessionRequest() {
         $auth = Zend_Auth::getInstance();
@@ -88,6 +101,10 @@ final class TaskManager {
     /**
      * Get the task_manager_lock from cc_pref with a row-level lock for atomicity
      *
+     * The lock is exclusive (prevent reads) and will only last for the duration
+     * of the transaction. We add NOWAIT so reads on the row during the transaction
+     * won't block
+     *
      * @return array|bool an array containing the row values, or false on failure
      */
     private function _getLock() {
diff --git a/airtime_mvc/application/forms/SoundCloudPreferences.php b/airtime_mvc/application/forms/SoundCloudPreferences.php
index fbf744520..591e811e8 100644
--- a/airtime_mvc/application/forms/SoundCloudPreferences.php
+++ b/airtime_mvc/application/forms/SoundCloudPreferences.php
@@ -10,30 +10,6 @@ class Application_Form_SoundcloudPreferences extends Zend_Form_SubForm
             array('ViewScript', array('viewScript' => 'form/preferences_soundcloud.phtml'))
         ));
 
-//        $select = new Zend_Form_Element_Select('SoundCloudTrackType');
-//        $select->setLabel(_('Default Track Type:'));
-//        $select->setAttrib('class', 'input_select');
-//        $select->setMultiOptions(array(
-//                "" => "",
-//                "original" => _("Original"),
-//                "remix" => _("Remix"),
-//                "live" => _("Live"),
-//                "recording" => _("Recording"),
-//                "spoken" => _("Spoken"),
-//                "podcast" => _("Podcast"),
-//                "demo" => _("Demo"),
-//                "in progress" => _("Work in progress"),
-//                "stem" => _("Stem"),
-//                "loop" => _("Loop"),
-//                "sound effect" => _("Sound Effect"),
-//                "sample" => _("One Shot Sample"),
-//                "other" => _("Other")
-//            ));
-//        $select->setRequired(false);
-//        $select->setValue(Application_Model_Preference::GetSoundCloudTrackType());
-//        $select->setDecorators(array('ViewHelper'));
-//        $this->addElement($select);
-
         $select = new Zend_Form_Element_Select('SoundCloudLicense');
         $select->setLabel(_('Default License:'));
         $select->setAttrib('class', 'input_select');

From 03d2ad54742bf1b7193560235625770f8779b6cf Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 18 Jun 2015 12:08:24 -0400
Subject: [PATCH 14/39] Run initctl and update rc.d when using --no-init-script

---
 python_apps/airtime-celery/setup.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 4604a2b31..279106964 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -15,14 +15,10 @@ else:
 
 
 def postinst():
-    print "Reloading initctl configuration"
-    call(['initctl', 'reload-configuration'])
     # Make /etc/init.d file executable and set proper
     # permissions for the defaults config file
     os.chmod('/etc/init.d/airtime-celery', 0755)
     os.chmod('/etc/default/airtime-celery', 0640)
-    # print "Setting Celery to start on boot"
-    call(['update-rc.d', 'airtime-celery', 'defaults'])
     print "Run \"sudo service airtime-celery restart\" now."
 
 setup(name='airtime-celery',
@@ -41,5 +37,10 @@ setup(name='airtime-celery',
       zip_safe=False,
       data_files=data_files)
 
+print "Reloading initctl configuration"
+call(['initctl', 'reload-configuration'])
+print "Setting Celery to start on boot"
+call(['update-rc.d', 'airtime-celery', 'defaults'])
+
 if data_files:
     postinst()

From 6439bc9ad51d6a541d54504dd1ae75cd2b57a714 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 18 Jun 2015 13:32:43 -0400
Subject: [PATCH 15/39] Add timestamp on SoundCloud upload success

---
 airtime_mvc/application/services/SoundcloudService.php      | 2 ++
 .../application/services/ThirdPartyCeleryService.php        | 6 +++---
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index 5ae0a734b..4aac783a7 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -102,6 +102,8 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
         $ref->setDbService(static::$_SERVICE_NAME);
         // Only set the SoundCloud fields if the task was successful
         if ($status == CELERY_SUCCESS_STATUS) {
+            $utc = new DateTimeZone("UTC");
+            $ref->setDbUploadTime(new DateTime("now", $utc));
             // TODO: fetch any additional SoundCloud parameters we want to store
             $ref->setDbForeignId($track->id);  // SoundCloud identifier
         }
diff --git a/airtime_mvc/application/services/ThirdPartyCeleryService.php b/airtime_mvc/application/services/ThirdPartyCeleryService.php
index ec3527545..5791c30ca 100644
--- a/airtime_mvc/application/services/ThirdPartyCeleryService.php
+++ b/airtime_mvc/application/services/ThirdPartyCeleryService.php
@@ -108,10 +108,10 @@ abstract class ThirdPartyCeleryService extends ThirdPartyService {
      * @throws PropelException
      */
     public function updateTrackReference($trackId, $track, $status) {
-        $ref = CeleryTasksQuery::create()
+        $task = CeleryTasksQuery::create()
             ->findOneByDbTrackReference($trackId);
-        $ref->setDbStatus($status);
-        $ref->save();
+        $task->setDbStatus($status);
+        $task->save();
     }
 
     /**

From de184d761eea28708fa778484d201dac6930bcbf Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 18 Jun 2015 13:49:25 -0400
Subject: [PATCH 16/39] Rename variable

---
 .../services/ThirdPartyCeleryService.php       | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/airtime_mvc/application/services/ThirdPartyCeleryService.php b/airtime_mvc/application/services/ThirdPartyCeleryService.php
index 5791c30ca..e4890a616 100644
--- a/airtime_mvc/application/services/ThirdPartyCeleryService.php
+++ b/airtime_mvc/application/services/ThirdPartyCeleryService.php
@@ -83,17 +83,17 @@ abstract class ThirdPartyCeleryService extends ThirdPartyService {
     protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
         $trackId = $this->createTrackReference($fileId);
         // First, check if the track already has an entry in the database
-        $ref = CeleryTasksQuery::create()->findOneByDbId($brokerTaskId);
-        if (is_null($ref)) {
-            $ref = new CeleryTasks();
+        $task = CeleryTasksQuery::create()->findOneByDbId($brokerTaskId);
+        if (is_null($task)) {
+            $task = new CeleryTasks();
         }
-        $ref->setDbId($brokerTaskId);
-        $ref->setDbName($taskName);
+        $task->setDbId($brokerTaskId);
+        $task->setDbName($taskName);
         $utc = new DateTimeZone("UTC");
-        $ref->setDbDispatchTime(new DateTime("now", $utc));
-        $ref->setDbStatus(CELERY_PENDING_STATUS);
-        $ref->setDbTrackReference($trackId);
-        $ref->save();
+        $task->setDbDispatchTime(new DateTime("now", $utc));
+        $task->setDbStatus(CELERY_PENDING_STATUS);
+        $task->setDbTrackReference($trackId);
+        $task->save();
     }
 
     /**

From 4a92ec4e12fa05bc8e4836397f1ebb82f5745277 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 18 Jun 2015 15:05:32 -0400
Subject: [PATCH 17/39] Fix airtime-celery setup

---
 python_apps/airtime-celery/setup.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 279106964..6e709170f 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -11,6 +11,8 @@ if sys.argv[1] in install_args and "--no-init-script" not in sys.argv:
     data_files = [('/etc/default', ['install/conf/airtime-celery']),
                   ('/etc/init.d', ['install/initd/airtime-celery'])]
 else:
+    if "--no-init-script" in sys.argv:
+        sys.argv.remove("--no-init-script")
     data_files = []
 
 
@@ -19,6 +21,10 @@ def postinst():
     # permissions for the defaults config file
     os.chmod('/etc/init.d/airtime-celery', 0755)
     os.chmod('/etc/default/airtime-celery', 0640)
+    print "Reloading initctl configuration"
+    call(['initctl', 'reload-configuration'])
+    print "Setting Celery to start on boot"
+    call(['update-rc.d', 'airtime-celery', 'defaults'])
     print "Run \"sudo service airtime-celery restart\" now."
 
 setup(name='airtime-celery',
@@ -37,10 +43,5 @@ setup(name='airtime-celery',
       zip_safe=False,
       data_files=data_files)
 
-print "Reloading initctl configuration"
-call(['initctl', 'reload-configuration'])
-print "Setting Celery to start on boot"
-call(['update-rc.d', 'airtime-celery', 'defaults'])
-
 if data_files:
     postinst()

From 17983167ed3163a71d5980d4eb87aad6fae8f08d Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 18 Jun 2015 15:14:42 -0400
Subject: [PATCH 18/39] Add configobj import to setup

---
 python_apps/airtime-celery/setup.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 6e709170f..bfac57fba 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -2,7 +2,6 @@ from setuptools import setup
 from subprocess import call
 import os
 import sys
-from glob import glob
 
 install_args = ['install', 'install_data', 'develop']
 
@@ -38,7 +37,8 @@ setup(name='airtime-celery',
       install_requires=[
           'soundcloud',
           'celery',
-          'kombu'
+          'kombu',
+          'configobj'
       ],
       zip_safe=False,
       data_files=data_files)

From 76a7aa9a245135d04e5d8e2dbf27b64623954b6c Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 18 Jun 2015 18:18:48 -0400
Subject: [PATCH 19/39] Fix SoundCloud and TaskManager bugs, switch tasks to
 use acks_late, and provide feedback from SoundCloud context menu items

---
 .../application/common/TaskManager.php        |  7 +++++-
 .../controllers/LibraryController.php         | 12 ++++++++--
 .../application/models/airtime/CcFiles.php    |  4 +++-
 .../services/ThirdPartyService.php            | 22 +++++++++++++++----
 .../public/js/airtime/library/library.js      | 11 ++++++++++
 .../airtime-celery/airtime-celery/tasks.py    |  4 ++--
 6 files changed, 50 insertions(+), 10 deletions(-)

diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php
index cec3a2f56..415afa55f 100644
--- a/airtime_mvc/application/common/TaskManager.php
+++ b/airtime_mvc/application/common/TaskManager.php
@@ -22,7 +22,7 @@ final class TaskManager {
      * @var int TASK_INTERVAL_SECONDS how often, in seconds, to run the TaskManager tasks,
      *                                if they need to be run
      */
-    const TASK_INTERVAL_SECONDS = 60;
+    const TASK_INTERVAL_SECONDS = 30;
 
     /**
      * @var $con PDO Propel connection object
@@ -67,6 +67,11 @@ final class TaskManager {
         try {
             $lock = $this->_getLock();
             if ($lock && microtime(true) < $lock['valstr'] + self::TASK_INTERVAL_SECONDS) {
+                // Fun fact: Propel caches the database connection and uses it persistently
+                // (thus why calling Propel::getConnection explicitly and passing a connection
+                // parameter is often not necessary when making Propel queries). Long story short,
+                // if we don't use commit() here, we end up blocking other queries made within this request
+                $this->_con->commit();
                 return;
             }
             $this->_updateLock($lock);
diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index 6b691f760..e2de84adf 100644
--- a/airtime_mvc/application/controllers/LibraryController.php
+++ b/airtime_mvc/application/controllers/LibraryController.php
@@ -278,9 +278,17 @@ class LibraryController extends Zend_Controller_Action
             $serviceId = $soundcloudService->getServiceId($id);
             if (!is_null($file) && $serviceId != 0) {
                 $menu["soundcloud"]["items"]["view"] = array("name" => _("View track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/view-on-sound-cloud/id/{$id}");
-                $menu["soundcloud"]["items"]["upload"] = array("name" => _("Remove track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/delete/id/{$id}");
+                $menu["soundcloud"]["items"]["remove"] = array("name" => _("Remove track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/delete/id/{$id}");
             } else {
-                $menu["soundcloud"]["items"]["upload"] = array("name" => _("Upload track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/upload/id/{$id}");
+                // If a reference exists for this file ID, that means the user has uploaded the track
+                // but we haven't yet gotten a response from Celery, so disable the menu item
+                if ($soundcloudService->referenceExists($id)) {
+                    $menu["soundcloud"]["items"]["upload"] = array("name" => _("Upload track"), "icon" => "soundcloud",
+                                                                   "url" => $baseUrl."soundcloud/upload/id/{$id}", "disabled" => true);
+                } else {
+                    $menu["soundcloud"]["items"]["upload"] = array("name" => _("Upload track"), "icon" => "soundcloud",
+                                                                   "url" => $baseUrl."soundcloud/upload/id/{$id}");
+                }
             }
         }
 
diff --git a/airtime_mvc/application/models/airtime/CcFiles.php b/airtime_mvc/application/models/airtime/CcFiles.php
index 4140485ea..16fb99c67 100644
--- a/airtime_mvc/application/models/airtime/CcFiles.php
+++ b/airtime_mvc/application/models/airtime/CcFiles.php
@@ -74,6 +74,8 @@ class CcFiles extends BaseCcFiles {
     /** Used to create a CcFiles object from an array containing metadata and a file uploaded by POST.
      *  This is used by our Media REST API!
      * @param $fileArray An array containing metadata for a CcFiles object.
+     *
+     * @return object the sanitized response
      * @throws Exception
      */
     public static function createFromUpload($fileArray)
@@ -94,7 +96,7 @@ class CcFiles extends BaseCcFiles {
         $tempFilePath = $_FILES['file']['tmp_name'];
 
         try {
-            self::createAndImport($fileArray, $tempFilePath, $originalFilename);
+            return self::createAndImport($fileArray, $tempFilePath, $originalFilename);
         } catch (Exception $e) {
             if (file_exists($tempFilePath)) {
                 unlink($tempFilePath);
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
index 365002ee7..988b24588 100644
--- a/airtime_mvc/application/services/ThirdPartyService.php
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -74,7 +74,7 @@ abstract class ThirdPartyService {
      * Given a CcFiles identifier for a file that's been uploaded to a third-party service,
      * return the third-party identifier for the remote file
      *
-     * @param int $fileId the local CcFiles identifier
+     * @param int $fileId the cc_files identifier
      *
      * @return string the service foreign identifier
      */
@@ -85,11 +85,25 @@ abstract class ThirdPartyService {
         return empty($ref) ? '' : $ref->getDbForeignId();
     }
 
+    /**
+     * Check if a reference exists for a given CcFiles identifier
+     *
+     * @param int $fileId the cc_files identifier
+     *
+     * @return string the service foreign identifier
+     */
+    public function referenceExists($fileId) {
+        $ref = ThirdPartyTrackReferencesQuery::create()
+            ->filterByDbService(static::$_SERVICE_NAME)
+            ->findOneByDbFileId($fileId);  // There shouldn't be duplicates!
+        return !empty($ref);
+    }
+
     /**
      * Given a CcFiles identifier for a file that's been uploaded to a third-party service,
      * return a link to the remote file
      *
-     * @param int $fileId CcFiles identifier
+     * @param int $fileId the cc_files identifier
      *
      * @return string the link to the remote file
      */
@@ -101,14 +115,14 @@ abstract class ThirdPartyService {
     /**
      * Upload the file with the given identifier to a third-party service
      *
-     * @param int $fileId CcFiles identifier
+     * @param int $fileId the cc_files identifier
      */
     abstract function upload($fileId);
 
     /**
      * Delete the file with the given identifier from a third-party service
      *
-     * @param int $fileId the local CcFiles identifier
+     * @param int $fileId the cc_files identifier
      *
      * @throws ServiceNotFoundException when a $fileId with no corresponding
      *                                  service identifier is given
diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js
index 445e36b5a..2f3bf87d0 100644
--- a/airtime_mvc/public/js/airtime/library/library.js
+++ b/airtime_mvc/public/js/airtime/library/library.js
@@ -1016,11 +1016,22 @@ var AIRTIME = (function(AIRTIME) {
                         if (soundcloud.upload !== undefined) {
 
                             callback = function() {
+                                alert($.i18n._("Your track is being uploaded to SoundCloud"));
                                 $.post(soundcloud.upload.url, function(){});
                             };
                             soundcloud.upload.callback = callback;
                         }
 
+                        // define an upload to soundcloud callback.
+                        if (soundcloud.remove !== undefined) {
+
+                            callback = function() {
+                                alert($.i18n._("Your track is being deleted from SoundCloud"));
+                                $.post(soundcloud.remove.url, function(){});
+                            };
+                            soundcloud.remove.callback = callback;
+                        }
+
                         // define a view on soundcloud callback
                         if (soundcloud.view !== undefined) {
 
diff --git a/python_apps/airtime-celery/airtime-celery/tasks.py b/python_apps/airtime-celery/airtime-celery/tasks.py
index bfe873609..27554241a 100644
--- a/python_apps/airtime-celery/airtime-celery/tasks.py
+++ b/python_apps/airtime-celery/airtime-celery/tasks.py
@@ -9,7 +9,7 @@ celery = Celery()
 logger = get_task_logger(__name__)
 
 
-@celery.task(name='soundcloud-upload')
+@celery.task(name='soundcloud-upload', acks_late=True)
 def soundcloud_upload(data, token, file_path):
     """
     Upload a file to SoundCloud
@@ -33,7 +33,7 @@ def soundcloud_upload(data, token, file_path):
     data['asset_data'].close()
     return json.dumps(track.fields())
 
-@celery.task(name='soundcloud-delete')
+@celery.task(name='soundcloud-delete', acks_late=True)
 def soundcloud_delete(token, track_id):
     """
     Delete a file from SoundCloud

From fa7e16fc9d97d083f7c5683a8ddb45e8cca972a9 Mon Sep 17 00:00:00 2001
From: Albert Santoni <albert.santoni@sourcefabric.org>
Date: Tue, 23 Jun 2015 11:54:08 -0400
Subject: [PATCH 20/39] Added celery user creation to README

---
 python_apps/airtime-celery/README.rst | 1 +
 1 file changed, 1 insertion(+)

diff --git a/python_apps/airtime-celery/README.rst b/python_apps/airtime-celery/README.rst
index f079f78f6..2065435d2 100644
--- a/python_apps/airtime-celery/README.rst
+++ b/python_apps/airtime-celery/README.rst
@@ -7,6 +7,7 @@ Communication and the Celery results backend are both handled with amqp (RabbitM
 Installation
 ============
 
+    $ sudo adduser --system --no-create-home --disabled-login --disabled-password --group celery
     $ sudo python setup.py install
 
 Each instance of airtime-celery has its own worker, and multiple instances can be run in parallel.

From 70f6cbbc71b1dbec80d2407ac37a0fed3d1082b8 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 23 Jun 2015 15:10:02 -0400
Subject: [PATCH 21/39] Fixes to airtime-celery setup

---
 .../application/common/TaskManager.php        |  6 ++--
 .../application/services/CeleryService.php    | 32 +++++++++++--------
 .../scripts/form/preferences_soundcloud.phtml | 14 --------
 python_apps/airtime-celery/README.rst         | 24 ++++++++++++++
 python_apps/airtime-celery/setup.py           | 13 +++++++-
 5 files changed, 57 insertions(+), 32 deletions(-)

diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php
index 415afa55f..aa250f294 100644
--- a/airtime_mvc/application/common/TaskManager.php
+++ b/airtime_mvc/application/common/TaskManager.php
@@ -67,10 +67,8 @@ final class TaskManager {
         try {
             $lock = $this->_getLock();
             if ($lock && microtime(true) < $lock['valstr'] + self::TASK_INTERVAL_SECONDS) {
-                // Fun fact: Propel caches the database connection and uses it persistently
-                // (thus why calling Propel::getConnection explicitly and passing a connection
-                // parameter is often not necessary when making Propel queries). Long story short,
-                // if we don't use commit() here, we end up blocking other queries made within this request
+                // Propel caches the database connection and uses it persistently, so if we don't
+                // use commit() here, we end up blocking other queries made within this request
                 $this->_con->commit();
                 return;
             }
diff --git a/airtime_mvc/application/services/CeleryService.php b/airtime_mvc/application/services/CeleryService.php
index 9f21af3e4..ad18a56b1 100644
--- a/airtime_mvc/application/services/CeleryService.php
+++ b/airtime_mvc/application/services/CeleryService.php
@@ -50,8 +50,6 @@ class CeleryService {
      *
      * @return string the task identifier for the started Celery task so we can fetch the
      *                results asynchronously later
-     *
-     * @throws CeleryException when no message is found
      */
     public static function sendCeleryMessage($task, $exchange, $data) {
         $config = parse_ini_file(Application_Model_RabbitMq::getRmqConfigPath(), true);
@@ -67,8 +65,9 @@ class CeleryService {
      *
      * @param $task CeleryTasks the Celery task object
      *
-     * @return object the message object
+     * @return array the message response array
      *
+     * @throws CeleryException        when no message is found
      * @throws CeleryTimeoutException when no message is found and more than
      *                                $_CELERY_MESSAGE_TIMEOUT milliseconds have passed
      */
@@ -80,13 +79,19 @@ class CeleryService {
 
         // If the message isn't ready yet (Celery hasn't finished the task),
         // only throw an exception if the message has timed out.
-        if ($message == FALSE && self::_checkMessageTimeout($task)) {
-            // If the task times out, mark it as failed. We don't want to remove the
-            // track reference here in case it was a deletion that failed, for example.
-            $task->setDbStatus(CELERY_FAILED_STATUS);
-            $task->save();
-            throw new CeleryTimeoutException("Celery task " . $task->getDbName()
-                                             . " with ID " . $task->getDbId() . " timed out");
+        if ($message == FALSE) {
+            if (self::_checkMessageTimeout($task)) {
+                // If the task times out, mark it as failed. We don't want to remove the
+                // track reference here in case it was a deletion that failed, for example.
+                $task->setDbStatus(CELERY_FAILED_STATUS)->save();
+                throw new CeleryTimeoutException("Celery task " . $task->getDbName()
+                                                 . " with ID " . $task->getDbId() . " timed out");
+            } else {
+                // The message hasn't timed out, but it's still false, which means it hasn't been
+                // sent back from Celery yet.
+                throw new CeleryException("Waiting on Celery task " . $task->getDbName()
+                                          . " with ID " . $task->getDbId());
+            }
         }
         return $message;
     }
@@ -121,7 +126,7 @@ class CeleryService {
                 $message = self::_getTaskMessage($task);
                 self::_processTaskMessage($task, $message);
             } catch (CeleryTimeoutException $e) {
-                Logging::info($e->getMessage());
+                Logging::warn($e->getMessage());
             } catch (Exception $e) {
                 // Because $message->result can be either an object or a string, sometimes
                 // we get a json_decode error and end up here
@@ -161,10 +166,11 @@ class CeleryService {
      *
      * @return object the task message object
      *
-     * @throws CeleryException when the result message for this task no longer exists
+     * @throws CeleryException when the result message for this task is still pending
+     * @throws CeleryTimeoutException when the result message for this task no longer exists
      */
     protected static function _getTaskMessage($task) {
-        $message =  self::getAsyncResultMessage($task);
+        $message = self::getAsyncResultMessage($task);
         return json_decode($message['body']);
     }
 
diff --git a/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml b/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml
index 594468474..2697a6508 100644
--- a/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml
+++ b/airtime_mvc/application/views/scripts/form/preferences_soundcloud.phtml
@@ -1,19 +1,5 @@
 <fieldset class="padded">
     <dl class="zend_form">
-<!--        <dt id="SoundCloudTrackType-label" class="block-display">-->
-<!--             <label class="optional" for="SoundCloudTrackType">--><?php //echo $this->element->getElement('SoundCloudTrackType')->getLabel() ?><!--</label>-->
-<!--        </dt>-->
-<!--        <dd id="SoundCloudTrackType-element" class="block-display">-->
-<!--            --><?php //echo $this->element->getElement('SoundCloudTrackType') ?>
-<!--            --><?php //if($this->element->getElement('SoundCloudTrackType')->hasErrors()) : ?>
-<!--                <ul class='errors'>-->
-<!--                    --><?php //foreach($this->element->getElement('SoundCloudTrackType')->getMessages() as $error): ?>
-<!--                        <li>--><?php //echo $error; ?><!--</li>-->
-<!--                    --><?php //endforeach; ?>
-<!--                </ul>-->
-<!--            --><?php //endif; ?>
-<!--        </dd>-->
-
         <?php
             $soundcloudService = new SoundcloudService();
             if ($soundcloudService->hasAccessToken()) {
diff --git a/python_apps/airtime-celery/README.rst b/python_apps/airtime-celery/README.rst
index f079f78f6..cd5e2b5f2 100644
--- a/python_apps/airtime-celery/README.rst
+++ b/python_apps/airtime-celery/README.rst
@@ -25,8 +25,16 @@ This program must be run with sudo:
 Developers
 ==========
 
+To debug, you can run celery directly from the command line:
+
+    $ cd /my/airtime/root/python_apps/airtime-celery
+    $ RMQ_CONFIG_FILE=/etc/airtime/airtime.conf celery -A airtime-celery.tasks worker --loglevel=info
+
+This worker can be run alongside the service without issue.
+
 You may want to use the setuptools develop target to install:
 
+    $ cd /my/airtime/root/python_apps/airtime-celery
     $ sudo python setup.py develop
 
 You will need to allow the "airtime" RabbitMQ user to access all exchanges and queues within the /airtime vhost:
@@ -39,3 +47,19 @@ Logging
 By default, logs are saved to:
 
     /var/log/airtime/airtime-celery[-DEV_ENV].log
+
+Troubleshooting
+===============
+
+If you run into issues getting Celery to accept tasks from Airtime:
+
+    1) Make sure Celery is running ($ sudo service airtime-celery status).
+
+    2) Check the log file (/var/log/airtime/airtime-celery[-DEV_ENV].log) to make sure Celery started correctly.
+
+    3) Check your /etc/airtime/airtime.conf rabbitmq settings. Make sure the settings here align with
+       /etc/airtime-saas/production/rabbitmq.ini.
+
+    4) Check RabbitMQ to make sure the celeryresults and task queues were created in the correct vhost.
+
+    5) Make sure the RabbitMQ user (the default is airtime) has permissions on all vhosts being used.
\ No newline at end of file
diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index bfac57fba..8832ac2a8 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -4,13 +4,16 @@ import os
 import sys
 
 install_args = ['install', 'install_data', 'develop']
+run_postinst = False
 
 # XXX Definitely not the best way of doing this...
 if sys.argv[1] in install_args and "--no-init-script" not in sys.argv:
+    run_postinst = True
     data_files = [('/etc/default', ['install/conf/airtime-celery']),
                   ('/etc/init.d', ['install/initd/airtime-celery'])]
 else:
     if "--no-init-script" in sys.argv:
+        run_postinst = True  # We still want to run the postinst here
         sys.argv.remove("--no-init-script")
     data_files = []
 
@@ -20,6 +23,14 @@ def postinst():
     # permissions for the defaults config file
     os.chmod('/etc/init.d/airtime-celery', 0755)
     os.chmod('/etc/default/airtime-celery', 0640)
+    # Make the airtime log directory group-writable
+    os.chmod('/var/log/airtime', 0775)
+
+    # Create the Celery user
+    call(['adduser', '--no-create-home', '--home', '/var/lib/celery', '--gecos', '""', '--disabled-login', 'celery'])
+    # Add celery to the www-data group
+    call(['usermod', '-G', 'www-data', '-a', 'celery'])
+
     print "Reloading initctl configuration"
     call(['initctl', 'reload-configuration'])
     print "Setting Celery to start on boot"
@@ -43,5 +54,5 @@ setup(name='airtime-celery',
       zip_safe=False,
       data_files=data_files)
 
-if data_files:
+if run_postinst:
     postinst()

From 9bce3e685090d06e0a925498a2df55998ac7b090 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 23 Jun 2015 15:11:53 -0400
Subject: [PATCH 22/39] Remove user creation from airtime-celery README,
 automated in setup

---
 python_apps/airtime-celery/README.rst | 1 -
 1 file changed, 1 deletion(-)

diff --git a/python_apps/airtime-celery/README.rst b/python_apps/airtime-celery/README.rst
index 2188ca343..78df52a42 100644
--- a/python_apps/airtime-celery/README.rst
+++ b/python_apps/airtime-celery/README.rst
@@ -7,7 +7,6 @@ Communication and the Celery results backend are both handled with amqp (RabbitM
 Installation
 ============
 
-    $ sudo adduser --system --no-create-home --disabled-login --disabled-password --group celery
     $ sudo python setup.py install
 
 Each instance of airtime-celery has its own worker, and multiple instances can be run in parallel.

From d48e594dcd626e7dc829312dcde83f8b5e1ac626 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 23 Jun 2015 19:02:28 -0400
Subject: [PATCH 23/39] Fix upload to SoundCloud button remaining disabled
 after upload failure

---
 .../application/services/ThirdPartyCeleryService.php       | 2 +-
 airtime_mvc/application/services/ThirdPartyService.php     | 7 ++++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/services/ThirdPartyCeleryService.php b/airtime_mvc/application/services/ThirdPartyCeleryService.php
index e4890a616..33d661e9b 100644
--- a/airtime_mvc/application/services/ThirdPartyCeleryService.php
+++ b/airtime_mvc/application/services/ThirdPartyCeleryService.php
@@ -83,7 +83,7 @@ abstract class ThirdPartyCeleryService extends ThirdPartyService {
     protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
         $trackId = $this->createTrackReference($fileId);
         // First, check if the track already has an entry in the database
-        $task = CeleryTasksQuery::create()->findOneByDbId($brokerTaskId);
+        $task = CeleryTasksQuery::create()->findOneByDbTrackReference($trackId);
         if (is_null($task)) {
             $task = new CeleryTasks();
         }
diff --git a/airtime_mvc/application/services/ThirdPartyService.php b/airtime_mvc/application/services/ThirdPartyService.php
index 988b24588..5af1eb0e4 100644
--- a/airtime_mvc/application/services/ThirdPartyService.php
+++ b/airtime_mvc/application/services/ThirdPartyService.php
@@ -96,7 +96,12 @@ abstract class ThirdPartyService {
         $ref = ThirdPartyTrackReferencesQuery::create()
             ->filterByDbService(static::$_SERVICE_NAME)
             ->findOneByDbFileId($fileId);  // There shouldn't be duplicates!
-        return !empty($ref);
+        if (!empty($ref)) {
+            $task = CeleryTasksQuery::create()
+                ->findOneByDbTrackReference($ref->getDbId());
+            return $task->getDbStatus() != CELERY_FAILED_STATUS;
+        }
+        return false;
     }
 
     /**

From 4c797cf10053ed89183249a7881026f24d07121f Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 23 Jun 2015 19:02:55 -0400
Subject: [PATCH 24/39] Update installer to work with saas branch

---
 .../application/configs/config-check.php      | 39 ++++++-----
 .../airtime-setup/forms/finish-settings.php   |  3 +-
 airtime_mvc/build/airtime-setup/load.php      |  6 +-
 airtime_mvc/public/setup/database-setup.php   | 67 +++++++------------
 airtime_mvc/public/setup/general-setup.php    | 19 ++----
 airtime_mvc/public/setup/media-setup.php      | 30 +++++++--
 airtime_mvc/public/setup/rabbitmq-setup.php   | 55 +++++++--------
 airtime_mvc/public/setup/setup-functions.php  | 42 +++++++-----
 install                                       | 35 ++++++----
 python_apps/airtime-celery/MANIFEST.in        |  1 +
 python_apps/airtime-celery/setup.py           |  5 ++
 python_apps/airtime_analyzer/setup.py         |  6 ++
 uninstall                                     |  4 ++
 13 files changed, 168 insertions(+), 144 deletions(-)
 create mode 100644 python_apps/airtime-celery/MANIFEST.in

diff --git a/airtime_mvc/application/configs/config-check.php b/airtime_mvc/application/configs/config-check.php
index 9cfb9927b..5ddf73295 100644
--- a/airtime_mvc/application/configs/config-check.php
+++ b/airtime_mvc/application/configs/config-check.php
@@ -7,17 +7,17 @@
  * along with steps to fix them if they're not found or misconfigured.
  */
 
-$phpDependencies = checkPhpDependencies();
-$externalServices = checkExternalServices();
-$zend = $phpDependencies["zend"];
-$postgres = $phpDependencies["postgres"];
+$phpDependencies    = checkPhpDependencies();
+$externalServices   = checkExternalServices();
+$zend               = $phpDependencies["zend"];
+$postgres           = $phpDependencies["postgres"];
 
-$database =      $externalServices["database"];
-$rabbitmq =      $externalServices["rabbitmq"];
+$database           = $externalServices["database"];
+$rabbitmq           = $externalServices["rabbitmq"];
 
-$pypo =          $externalServices["pypo"];
-$liquidsoap =    $externalServices["liquidsoap"];
-$mediamonitor = $externalServices["media-monitor"];
+$pypo               = $externalServices["pypo"];
+$liquidsoap         = $externalServices["liquidsoap"];
+$analyzer       = $externalServices["analyzer"];
 
 $r1 = array_reduce($phpDependencies, "booleanReduce", true);
 $r2 = array_reduce($externalServices, "booleanReduce", true);
@@ -174,28 +174,27 @@ $result = $r1 && $r2;
                             Make sure RabbitMQ is installed correctly, and that your settings in /etc/airtime/airtime.conf
                             are correct. Try using <code>sudo rabbitmqctl list_users</code> and <code>sudo rabbitmqctl list_vhosts</code>
                             to see if the airtime user (or your custom RabbitMQ user) exists, then checking that 
-                            <code>sudo rabbitmqctl list_exchanges</code> contains entries for airtime-media-monitor, airtime-pypo, 
-                            and airtime-uploads.
+                            <code>sudo rabbitmqctl list_exchanges</code> contains entries for airtime-pypo and airtime-uploads.
                         <?php
                         }
                         ?>
                     </td>
                 </tr>
-                <tr class="<?=$mediamonitor ? 'success' : 'danger';?>">
+                <tr class="<?=$analyzer ? 'success' : 'danger';?>">
                     <td class="component">
-                        Media Monitor
+                        Airtime Analyzer
                     </td>
                     <td class="description">
-                        Airtime media-monitor service
+                        Airtime Upload and File Analysis service
                     </td>
-                    <td class="solution <?php if ($mediamonitor) {echo 'check';?>">
+                    <td class="solution <?php if ($analyzer) {echo 'check';?>">
                         <?php
                         } else {
                             ?>">
-                            Check that the airtime-media-monitor service is installed correctly in <code>/etc/init</code>, 
+                            Check that the airtime_analyzer service is installed correctly in <code>/etc/init.d</code>,
                             and ensure that it's running with
-                            <br/><code>initctl list | grep airtime-media-monitor</code><br/>
-                            If not, try running <code>sudo service airtime-media-monitor start</code>
+                            <br/><code>initctl list | grep airtime_analyzer</code><br/>
+                            If not, try running <code>sudo service airtime_analyzer start</code>
                         <?php
                         }
                         ?>
@@ -212,7 +211,7 @@ $result = $r1 && $r2;
                         <?php
                         } else {
                             ?>">
-                            Check that the airtime-playout service is installed correctly in <code>/etc/init</code>, 
+                            Check that the airtime-playout service is installed correctly in <code>/etc/init.d</code>,
                             and ensure that it's running with
                             <br/><code>initctl list | grep airtime-playout</code><br/>
                             If not, try running <code>sudo service airtime-playout restart</code>
@@ -232,7 +231,7 @@ $result = $r1 && $r2;
                         <?php
                         } else {
                             ?>">
-                            Check that the airtime-liquidsoap service is installed correctly in <code>/etc/init</code>, 
+                            Check that the airtime-liquidsoap service is installed correctly in <code>/etc/init.d</code>,
                             and ensure that it's running with
                             <br/><code>initctl list | grep airtime-liquidsoap</code><br/>
                             If not, try running <code>sudo service airtime-liquidsoap restart</code>
diff --git a/airtime_mvc/build/airtime-setup/forms/finish-settings.php b/airtime_mvc/build/airtime-setup/forms/finish-settings.php
index d6b298c34..e08afe4fc 100644
--- a/airtime_mvc/build/airtime-setup/forms/finish-settings.php
+++ b/airtime_mvc/build/airtime-setup/forms/finish-settings.php
@@ -9,7 +9,8 @@
     </p>
     <pre style="text-align: left">sudo service airtime-playout start
 sudo service airtime-liquidsoap start
-sudo service airtime-media-monitor start</pre>
+sudo service airtime_analyzer start
+sudo service airtime-celery start</pre>
     <p>
         Click "Done!" to bring up the Airtime configuration checklist; if your configuration is all green, 
         you're ready to get started with your personal Airtime station!
diff --git a/airtime_mvc/build/airtime-setup/load.php b/airtime_mvc/build/airtime-setup/load.php
index 722d6aec8..107b02b35 100644
--- a/airtime_mvc/build/airtime-setup/load.php
+++ b/airtime_mvc/build/airtime-setup/load.php
@@ -63,7 +63,7 @@ function checkDatabaseDependencies() {
 function checkExternalServices() {
     return array(
             "database" => checkDatabaseConfiguration(),
-            "media-monitor" => checkMediaMonitorService(),
+            "analyzer" => checkAnalyzerService(),
             "pypo" => checkPlayoutService(),
             "liquidsoap" => checkLiquidsoapService(),
             "rabbitmq" => checkRMQConnection()
@@ -123,8 +123,8 @@ function checkRMQConnection() {
  * 
  * @return boolean true if airtime-media-monitor is running
  */
-function checkMediaMonitorService() {
-    exec("pgrep -f -u www-data airtime-media-monitor", $out, $status);
+function checkAnalyzerService() {
+    exec("pgrep -f -u www-data airtime_analyzer", $out, $status);
     if (array_key_exists(0, $out) && $status == 0) {
         return posix_kill(rtrim($out[0]), 0);
     }
diff --git a/airtime_mvc/public/setup/database-setup.php b/airtime_mvc/public/setup/database-setup.php
index 821f549cf..f09b3a7ed 100644
--- a/airtime_mvc/public/setup/database-setup.php
+++ b/airtime_mvc/public/setup/database-setup.php
@@ -9,7 +9,7 @@
 class DatabaseSetup extends Setup {
 
     // airtime.conf section header
-    const SECTION = "[database]";
+    protected static $_settings = "[database]";
 
     // Constant form field names for passing errors back to the front-end
     const DB_USER = "dbUser",
@@ -17,31 +17,26 @@ class DatabaseSetup extends Setup {
         DB_NAME = "dbName",
         DB_HOST = "dbHost";
 
-    // Form field values
-    private $user, $pass, $name, $host;
-
     // Array of key->value pairs for airtime.conf
-    static $properties;
+    protected static $_properties;
 
+    /**
+     * @var PDO
+     */
     static $dbh = null;
 
     public function __construct($settings) {
-        $this->user = $settings[self::DB_USER];
-        $this->pass = $settings[self::DB_PASS];
-        $this->name = $settings[self::DB_NAME];
-        $this->host = $settings[self::DB_HOST];
-
-        self::$properties = array(
-            "host" => $this->host,
-            "dbname" => $this->name,
-            "dbuser" => $this->user,
-            "dbpass" => $this->pass,
+        static::$_properties = array(
+            "host"   => $settings[self::DB_HOST],
+            "dbname" => $settings[self::DB_NAME],
+            "dbuser" => $settings[self::DB_USER],
+            "dbpass" => $settings[self::DB_PASS],
         );
     }
 
     private function setNewDatabaseConnection($dbName) {
-        self::$dbh = new PDO("pgsql:host=" . $this->host . ";dbname=" . $dbName . ";port=5432"
-                             . ";user=" . $this->user . ";password=" . $this->pass);
+        self::$dbh = new PDO("pgsql:host=" . self::$_properties["host"] . ";dbname=" . $dbName . ";port=5432"
+                             . ";user=" . self::$_properties["dbuser"] . ";password=" . self::$_properties["dbpass"]);
         $err = self::$dbh->errorInfo();
         if ($err[1] != null) {
             throw new PDOException();
@@ -69,11 +64,7 @@ class DatabaseSetup extends Setup {
             throw new AirtimeDatabaseException("Couldn't establish a connection to the database! "
                                                . "Please check your credentials and try again. "
                                                . "PDO Exception: " .  $e->getMessage(),
-                                               array(
-                                                   self::DB_NAME,
-                                                   self::DB_USER,
-                                                   self::DB_PASS,
-                                               ));
+                                               array(self::DB_NAME, self::DB_USER, self::DB_PASS));
         }
 
         $this->writeToTemp();
@@ -85,13 +76,9 @@ class DatabaseSetup extends Setup {
         );
     }
 
-    protected function writeToTemp() {
-        parent::writeToTemp(self::SECTION, self::$properties);
-    }
-
     private function installDatabaseTables() {
         $this->checkDatabaseEncoding();
-        $this->setNewDatabaseConnection($this->name);
+        $this->setNewDatabaseConnection(self::$_properties["dbname"]);
         $this->checkSchemaExists();
         $this->createDatabaseTables();
     }
@@ -102,7 +89,7 @@ class DatabaseSetup extends Setup {
      */
     private function checkDatabaseExists() {
         $statement = self::$dbh->prepare("SELECT datname FROM pg_database WHERE datname = :dbname");
-        $statement->execute(array(":dbname" => $this->name));
+        $statement->execute(array(":dbname" => self::$_properties["dbname"]));
         $result = $statement->fetch();
         return isset($result[0]);
     }
@@ -126,16 +113,13 @@ class DatabaseSetup extends Setup {
      */
     private function checkUserCanCreateDb() {
         $statement = self::$dbh->prepare("SELECT 1 FROM pg_roles WHERE rolname=:dbuser AND rolcreatedb='t'");
-        $statement->execute(array(":dbuser" => $this->user));
+        $statement->execute(array(":dbuser" => self::$_properties["dbuser"]));
         $result = $statement->fetch();
         if (!isset($result[0])) {
-            throw new AirtimeDatabaseException("No database " . $this->name . " exists; user '" . $this->user
-                                               . "' does not have permission to create databases on " . $this->host,
-                                               array(
-                                                   self::DB_NAME,
-                                                   self::DB_USER,
-                                                   self::DB_PASS,
-                                               ));
+            throw new AirtimeDatabaseException("No database " . self::$_properties["dbname"] . " exists; user '"
+                                               . self::$_properties["dbuser"] . "' does not have permission to "
+                                               . "create databases on " . self::$_properties["host"],
+                                               array(self::DB_NAME, self::DB_USER, self::DB_PASS));
         }
     }
 
@@ -144,9 +128,9 @@ class DatabaseSetup extends Setup {
      * @throws AirtimeDatabaseException
      */
     private function createDatabase() {
-        $statement = self::$dbh->prepare("CREATE DATABASE " . pg_escape_string($this->name)
+        $statement = self::$dbh->prepare("CREATE DATABASE " . pg_escape_string(self::$_properties["dbname"])
                                          . " WITH ENCODING 'UTF8' TEMPLATE template0"
-                                         . " OWNER " . pg_escape_string($this->user));
+                                         . " OWNER " . pg_escape_string(self::$_properties["dbuser"]));
         if (!$statement->execute()) {
             throw new AirtimeDatabaseException("There was an error creating the database!",
                                                array(self::DB_NAME,));
@@ -169,8 +153,9 @@ class DatabaseSetup extends Setup {
                  * have multiple issues; they similarly die on any SQL errors, fail to read in multi-line
                  * commands, and fail on any unescaped ? or $ characters.
                  */
-                exec("export PGPASSWORD=" . $this->pass . " && psql -U " . $this->user . " --dbname "
-                     . $this->name . " -h " . $this->host . " -f $sqlDir$f 2>/dev/null", $out, $status);
+                exec("export PGPASSWORD=" . self::$_properties["dbpass"] . " && psql -U " . self::$_properties["dbuser"]
+                     . " --dbname " . self::$_properties["dbname"] . " -h " . self::$_properties["host"]
+                     . " -f $sqlDir$f 2>/dev/null", $out, $status);
             } catch (Exception $e) {
                 throw new AirtimeDatabaseException("There was an error setting up the Airtime schema!",
                                                    array(self::DB_NAME,));
@@ -185,7 +170,7 @@ class DatabaseSetup extends Setup {
     private function checkDatabaseEncoding() {
         $statement = self::$dbh->prepare("SELECT pg_encoding_to_char(encoding) "
                                          . "FROM pg_database WHERE datname = :dbname");
-        $statement->execute(array(":dbname" => $this->name));
+        $statement->execute(array(":dbname" => self::$_properties["dbname"]));
         $encoding = $statement->fetch();
         if (!($encoding && $encoding[0] == "UTF8")) {
             throw new AirtimeDatabaseException("The database was installed with an incorrect encoding type!",
diff --git a/airtime_mvc/public/setup/general-setup.php b/airtime_mvc/public/setup/general-setup.php
index f63a84f90..7e86bdb69 100644
--- a/airtime_mvc/public/setup/general-setup.php
+++ b/airtime_mvc/public/setup/general-setup.php
@@ -11,37 +11,28 @@
 class GeneralSetup extends Setup {
 
     // airtime.conf section header
-    const SECTION = "[general]";
+    protected static $_section = "[general]";
 
     // Constant form field names for passing errors back to the front-end
     const GENERAL_PORT = "generalPort",
         GENERAL_HOST = "generalHost";
 
-    // Form field values
-    static $user, $host, $port, $root;
-
     // Array of key->value pairs for airtime.conf
-    static $properties;
+    protected static $_properties;
 
     // Message and error fields to return to the front-end
     static $message = null;
     static $errors = array();
 
     function __construct($settings) {
-        self::$host = $settings[self::GENERAL_HOST];
-        self::$port = $settings[self::GENERAL_PORT];
 
-        self::$properties = array(
+        self::$_properties = array(
             "api_key" => $this->generateRandomString(),
-            "base_url" => self::$host,
-            "base_port" => self::$port,
+            "base_url" => $settings[self::GENERAL_HOST],
+            "base_port" => $settings[self::GENERAL_PORT],
         );
     }
 
-    function writeToTemp() {
-        parent::writeToTemp(self::SECTION, self::$properties);
-    }
-
     /**
      * @return array associative array containing a display message and fields with errors
      */
diff --git a/airtime_mvc/public/setup/media-setup.php b/airtime_mvc/public/setup/media-setup.php
index d2cb99edc..701ba0749 100644
--- a/airtime_mvc/public/setup/media-setup.php
+++ b/airtime_mvc/public/setup/media-setup.php
@@ -26,7 +26,9 @@ class MediaSetup extends Setup {
 
     const MEDIA_FOLDER = "mediaFolder";
     const AIRTIME_CONF_PATH = "/etc/airtime/airtime.conf";
-    
+    const RMQ_INI_BASE_PATH = "/etc/airtime-saas/";
+    const RMQ_INI_FILE_NAME = "rabbitmq-analyzer.ini";
+
     static $path;
     static $message = null;
     static $errors = array();
@@ -62,10 +64,14 @@ class MediaSetup extends Setup {
         // Finalize and move airtime.conf.temp
         if (file_exists("/etc/airtime/")) {
             if (!$this->moveAirtimeConfig()) {
-                $message = "Error moving airtime.conf or deleting /tmp/airtime.conf.temp!";
-                $errors[] = "ERR";
+                self::$message = "Error moving airtime.conf or deleting /tmp/airtime.conf.temp!";
+                self::$errors[] = "ERR";
             }
-            
+            if (!$this->moveRmqConfig()) {
+                self::$message = "Error moving rabbitmq-analyzer.ini or deleting /tmp/rabbitmq.ini.tmp!";
+                self::$errors[] = "ERR";
+            }
+
             /* 
              * If we're upgrading from an old Airtime instance (pre-2.5.2) we rename their old 
              * airtime.conf to airtime.conf.tmp during the setup process. Now that we're done,
@@ -75,8 +81,8 @@ class MediaSetup extends Setup {
                 rename(self::AIRTIME_CONF_PATH . ".tmp", self::AIRTIME_CONF_PATH . ".bak");
             }
         } else {
-            $message = "Failed to move airtime.conf; /etc/airtime doesn't exist!";
-            $errors[] = "ERR";
+            self::$message = "Failed to move airtime.conf; /etc/airtime doesn't exist!";
+            self::$errors[] = "ERR";
         }
         
         return array(
@@ -84,7 +90,7 @@ class MediaSetup extends Setup {
             "errors" => self::$errors
         );
     }
-    
+
     /**
      * Moves /tmp/airtime.conf.temp to /etc/airtime.conf and then removes it to complete setup
      * @return boolean false if either of the copy or removal operations fail
@@ -94,6 +100,16 @@ class MediaSetup extends Setup {
             && unlink(AIRTIME_CONF_TEMP_PATH);
     }
 
+    /**
+     * Moves /tmp/airtime.conf.temp to /etc/airtime.conf and then removes it to complete setup
+     * @return boolean false if either of the copy or removal operations fail
+     */
+    function moveRmqConfig() {
+        return copy(RMQ_INI_TEMP_PATH, self::RMQ_INI_BASE_PATH . self::RMQ_INI_FILE_NAME)
+            && copy(RMQ_INI_TEMP_PATH, self::RMQ_INI_BASE_PATH . "production/" . self::RMQ_INI_FILE_NAME)
+            && unlink(RMQ_INI_TEMP_PATH);
+    }
+
     /**
      * Add the given directory to cc_music_dirs
      * TODO Should we check for an existing entry in cc_music_dirs?
diff --git a/airtime_mvc/public/setup/rabbitmq-setup.php b/airtime_mvc/public/setup/rabbitmq-setup.php
index 817b51742..e8d210fec 100644
--- a/airtime_mvc/public/setup/rabbitmq-setup.php
+++ b/airtime_mvc/public/setup/rabbitmq-setup.php
@@ -13,7 +13,10 @@ require_once dirname(dirname( __DIR__)) . '/library/php-amqplib/amqp.inc';
 class RabbitMQSetup extends Setup {
 
     // airtime.conf section header
-    const SECTION = "[rabbitmq]";
+    protected static $_section = "[rabbitmq]";
+
+    // Array of key->value pairs for airtime.conf
+    protected static $_properties;
 
     // Constant form field names for passing errors back to the front-end
     const RMQ_USER = "rmqUser",
@@ -22,29 +25,17 @@ class RabbitMQSetup extends Setup {
         RMQ_HOST = "rmqHost",
         RMQ_VHOST = "rmqVHost";
 
-    // Form field values
-    static $user, $pass, $host, $port, $vhost;
-
-    // Array of key->value pairs for airtime.conf
-    static $properties;
-
     // Message and error fields to return to the front-end
     static $message = null;
     static $errors = array();
 
     function __construct($settings) {
-        self::$user = $settings[self::RMQ_USER];
-        self::$pass = $settings[self::RMQ_PASS];
-        self::$port = $settings[self::RMQ_PORT];
-        self::$host = $settings[self::RMQ_HOST];
-        self::$vhost = $settings[self::RMQ_VHOST];
-
-        self::$properties = array(
-            "host" => self::$host,
-            "port" => self::$port,
-            "user" => self::$user,
-            "password" => self::$pass,
-            "vhost" => self::$vhost,
+        static::$_properties = array(
+            "host"      => $settings[self::RMQ_HOST],
+            "port"      => $settings[self::RMQ_PORT],
+            "user"      => $settings[self::RMQ_USER],
+            "password"  => $settings[self::RMQ_PASS],
+            "vhost"     => $settings[self::RMQ_VHOST],
         );
     }
 
@@ -72,24 +63,20 @@ class RabbitMQSetup extends Setup {
         );
     }
 
-    function writeToTemp() {
-        parent::writeToTemp(self::SECTION, self::$properties);
-    }
-
     function checkRMQConnection() {
-        $conn = new AMQPConnection(self::$host,
-                                   self::$port,
-                                   self::$user,
-                                   self::$pass,
-                                   self::$vhost);
+        $conn = new AMQPConnection(self::$_properties["host"],
+                                   self::$_properties["port"],
+                                   self::$_properties["user"],
+                                   self::$_properties["password"],
+                                   self::$_properties["vhost"]);
         return isset($conn);
     }
 
     function identifyRMQConnectionError() {
         // It's impossible to identify errors coming out of amqp.inc without a major
         // rewrite, so for now just tell the user ALL THE THINGS went wrong
-        self::$message = "Couldn't connect to RabbitMQ server! Please check if the server "
-            . "is running and your credentials are correct.";
+        self::$message = _("Couldn't connect to RabbitMQ server! Please check if the server "
+            . "is running and your credentials are correct.");
         self::$errors[] = self::RMQ_USER;
         self::$errors[] = self::RMQ_PASS;
         self::$errors[] = self::RMQ_HOST;
@@ -97,4 +84,12 @@ class RabbitMQSetup extends Setup {
         self::$errors[] = self::RMQ_VHOST;
     }
 
+    protected function writeToTemp() {
+        if (!file_exists(RMQ_INI_TEMP_PATH)) {
+            copy(BUILD_PATH . "rabbitmq-analyzer.ini", RMQ_INI_TEMP_PATH);
+        }
+        $this->_write(RMQ_INI_TEMP_PATH);
+        parent::writeToTemp();
+    }
+
 }
\ No newline at end of file
diff --git a/airtime_mvc/public/setup/setup-functions.php b/airtime_mvc/public/setup/setup-functions.php
index 36d96265e..3e226b848 100644
--- a/airtime_mvc/public/setup/setup-functions.php
+++ b/airtime_mvc/public/setup/setup-functions.php
@@ -1,6 +1,7 @@
 <?php
 define("BUILD_PATH", dirname(dirname(__DIR__)) . "/build/");
 define("AIRTIME_CONF_TEMP_PATH", "/tmp/airtime.conf.temp");
+define("RMQ_INI_TEMP_PATH", "/tmp/rabbitmq.ini.tmp");
 
 /**
  * Class Setup
@@ -11,50 +12,59 @@ define("AIRTIME_CONF_TEMP_PATH", "/tmp/airtime.conf.temp");
  */
 abstract class Setup {
 
+    protected static $_section;
+
+    /**
+     * Array of key->value pairs for airtime.conf
+     *
+     * @var array
+     */
+    protected static $_properties;
+
     abstract function __construct($settings);
 
     abstract function runSetup();
 
     /**
      * Write new property values to a given section in airtime.conf.temp
-     *
-     * @param string $section
-     *            the configuration section to write to
-     * @param array $properties
-     *            the configuration properties and values to overwrite
      */
-    protected function writeToTemp($section, $properties) {
+    protected function writeToTemp() {
         if (!file_exists(AIRTIME_CONF_TEMP_PATH)) {
             copy(BUILD_PATH . "airtime.example.conf", AIRTIME_CONF_TEMP_PATH);
         }
-        
-        $file = file(AIRTIME_CONF_TEMP_PATH);
+
+        $this->_write(AIRTIME_CONF_TEMP_PATH);
+    }
+
+    protected function _write($filePath) {
+        $file = file($filePath);
         $fileOutput = "";
-        
+
         $inSection = false;
-        
+
         foreach ($file as $line) {
-            if (strpos($line, $section) !== false) {
+            if (strpos($line, static::$_section) !== false) {
                 $inSection = true;
             } else if (strpos($line, "[") !== false) {
                 $inSection = false;
             }
-            
+
             if (substr(trim($line), 0, 1) == "#") {
                 /* Workaround to strip comments from airtime.conf.
                  * We need to do this because python's ConfigObj and PHP
-                 * have different (and equally strict) rules about comment 
+                 * have different (and equally strict) rules about comment
                  * characters in configuration files.
                  */
             } else if ($inSection) {
                 $key = trim(@substr($line, 0, strpos($line, "=")));
-                $fileOutput .= $key && isset($properties[$key]) ? $key . " = " . $properties[$key] . "\n" : $line;
+                $fileOutput .= $key && isset(static::$_properties[$key]) ?
+                    $key . " = " . static::$_properties[$key] . "\n" : $line;
             } else {
                 $fileOutput .= $line;
             }
         }
-        
-        file_put_contents(AIRTIME_CONF_TEMP_PATH, $fileOutput);
+
+        file_put_contents($filePath, $fileOutput);
     }
 
     /**
diff --git a/install b/install
index 0cae36632..999501670 100755
--- a/install
+++ b/install
@@ -320,9 +320,10 @@ if [ -f /etc/airtime/airtime.conf ]; then
     
         set +e
         verbose "Stopping airtime services..."
-        loudCmd "service airtime-playout stop-with-monit"
-        loudCmd "service airtime-media-monitor stop-with-monit"
-        loudCmd "service airtime-liquidsoap stop-with-monit"
+        loudCmd "service airtime-playout stop"
+        loudCmd "service airtime-media-monitor stop"
+        loudCmd "service airtime_analyzer stop"
+        loudCmd "service airtime-liquidsoap stop"
         verbose "...Done"
         
         echo "Looks like you have an old version of Airtime. Your current /etc/airtime/airtime.conf \
@@ -332,7 +333,7 @@ will be moved to /etc/airtime/airtime.conf.tmp"
         if [ -d /usr/lib/airtime/ ]; then
             rm -rf /usr/lib/airtime/
         fi
-        
+
         rm /etc/init.d/airtime-*
     
         if [ "$apache" = "t" ]; then
@@ -459,12 +460,16 @@ verbose "\n * Installing API client..."
 loudCmd "python ${AIRTIMEROOT}/python_apps/api_clients/setup.py install --install-scripts=/usr/bin"
 verbose "...Done"
 
-verbose "\n * Installing media-monitor..."
-loudCmd "python ${AIRTIMEROOT}/python_apps/media-monitor/setup.py install --install-scripts=/usr/bin"
+verbose "\n * Installing pypo and liquidsoap..."
+loudCmd "python ${AIRTIMEROOT}/python_apps/pypo/setup.py install --install-scripts=/usr/bin"
 verbose "...Done"
 
-verbose "\n * Installing pypo..."
-loudCmd "python ${AIRTIMEROOT}/python_apps/pypo/setup.py install --install-scripts=/usr/bin"
+verbose "\n * Installing airtime-celery..."
+loudCmd "python ${AIRTIMEROOT}/python_apps/airtime-celery/setup.py install"
+verbose "...Done"
+
+verbose "\n * Installing airtime_analyzer..."
+loudCmd "python ${AIRTIMEROOT}/python_apps/airtime_analyzer/setup.py install --install-scripts=/usr/bin"
 verbose "...Done"
 
 #for i in /etc/init/airtime*.template; do
@@ -476,9 +481,10 @@ verbose "...Done"
 set +e
 loudCmd "initctl reload-configuration"
 loudCmd "systemctl daemon-reload" #systemd hipsters
-loudCmd "update-rc.d airtime-playout defaults" # Start at bootup, on Debian
-loudCmd "update-rc.d airtime-media-monitor defaults" # Start at bootup, on Debian
-loudCmd "update-rc.d airtime-liquidsoap defaults" # Start at bootup, on Debian
+loudCmd "update-rc.d airtime-playout defaults"      # Start at bootup, on Debian
+loudCmd "update-rc.d airtime-celery defaults"       # Start at bootup, on Debian
+loudCmd "update-rc.d airtime-liquidsoap defaults"   # Start at bootup, on Debian
+loudCmd "update-rc.d airtime_analyzer defaults"   # Start at bootup, on Debian
 set -e
 
 if [ ! -d /var/log/airtime ]; then
@@ -566,7 +572,8 @@ else
 fi
 
 verbose "\n * Setting RabbitMQ user permissions..."
-loudCmd "rabbitmqctl set_permissions -p ${RABBITMQ_VHOST} ${RABBITMQ_USER} \"$EXCHANGES\" \"$EXCHANGES\" \"$EXCHANGES\""
+#loudCmd "rabbitmqctl set_permissions -p ${RABBITMQ_VHOST} ${RABBITMQ_USER} \"$EXCHANGES\" \"$EXCHANGES\" \"$EXCHANGES\""
+loudCmd "rabbitmqctl set_permissions -p ${RABBITMQ_VHOST} ${RABBITMQ_USER} .\* .\* .\*"
 
 if [ ! -d "/etc/airtime" ]; then
     loud "\n-----------------------------------------------------"
@@ -575,8 +582,12 @@ if [ ! -d "/etc/airtime" ]; then
 
     verbose "\n * Creating /etc/airtime/ directory..."
     mkdir /etc/airtime
+    verbose "\n * Creating /etc/airtime-saas/ directory..."
+    # Default saas directory for the analyzer, cloud storage, and celery
+    mkdir -p /etc/airtime-saas/production
 fi
 chown -R ${web_user}:${web_user} /etc/airtime
+chown -R ${web_user}:${web_user} /etc/airtime-saas
 
 if [ ! -d "/srv/airtime" ]; then
     mkdir -p /srv/airtime
diff --git a/python_apps/airtime-celery/MANIFEST.in b/python_apps/airtime-celery/MANIFEST.in
new file mode 100644
index 000000000..9561fb106
--- /dev/null
+++ b/python_apps/airtime-celery/MANIFEST.in
@@ -0,0 +1 @@
+include README.rst
diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 8832ac2a8..3285aa785 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -3,6 +3,11 @@ from subprocess import call
 import os
 import sys
 
+# Change directory since setuptools uses relative paths
+script_path = os.path.dirname(os.path.realpath(__file__))
+print script_path
+os.chdir(script_path)
+
 install_args = ['install', 'install_data', 'develop']
 run_postinst = False
 
diff --git a/python_apps/airtime_analyzer/setup.py b/python_apps/airtime_analyzer/setup.py
index 0e57ac076..47e231123 100644
--- a/python_apps/airtime_analyzer/setup.py
+++ b/python_apps/airtime_analyzer/setup.py
@@ -1,6 +1,12 @@
 from setuptools import setup
 from subprocess import call
 import sys
+import os
+
+# Change directory since setuptools uses relative paths
+script_path = os.path.dirname(os.path.realpath(__file__))
+print script_path
+os.chdir(script_path)
 
 # Allows us to avoid installing the upstart init script when deploying airtime_analyzer
 # on Airtime Pro:
diff --git a/uninstall b/uninstall
index 1f3c20fc3..b0933aac3 100755
--- a/uninstall
+++ b/uninstall
@@ -23,6 +23,7 @@ dropAirtimeDatabase() {
     su postgres <<'EOF'
     set +e
     # DROP DATABASE cannot be executed from a function or multi-command string
+    psql -d postgres -tAc "DROP DATABASE IF EXISTS airtime_test"
     psql -d postgres -tAc "DROP DATABASE IF EXISTS airtime"
     psql -d postgres -tAc "DROP USER IF EXISTS airtime"
     set -e
@@ -87,7 +88,10 @@ else
 fi
 
 echo -e "\nUninstalling Airtime..."
+service airtime-celery stop
+set +e
 removeRabbitmqAirtimeSettings
+set -e
 
 for i in ${FILES[*]}; do
     rm -rf $i

From 065988f7033b57d8cd3f102bb320413bcca51d7e Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Tue, 23 Jun 2015 19:08:49 -0400
Subject: [PATCH 25/39] Return default value for disk quota if none set

---
 airtime_mvc/application/models/Preference.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index a03fd2e1b..bc5746b8e 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -1032,7 +1032,7 @@ class Application_Model_Preference
     public static function GetDiskQuota()
     {
         $val = self::getValue("disk_quota");
-        return (strlen($val) == 0) ? 0 : $val;
+        return empty($val) ? 2147483648 : $val;  # If there is no value for disk quota, return 2GB
     }
 
     public static function SetLiveStreamMasterUsername($value)

From eeb1f7eff02954c9eedc59fe8672ff3567a51cfb Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 24 Jun 2015 11:11:46 -0400
Subject: [PATCH 26/39] Fix airtime-celery setup

---
 python_apps/airtime-celery/setup.py | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/python_apps/airtime-celery/setup.py b/python_apps/airtime-celery/setup.py
index 3285aa785..8abbe2ce2 100644
--- a/python_apps/airtime-celery/setup.py
+++ b/python_apps/airtime-celery/setup.py
@@ -9,6 +9,7 @@ print script_path
 os.chdir(script_path)
 
 install_args = ['install', 'install_data', 'develop']
+no_init = False
 run_postinst = False
 
 # XXX Definitely not the best way of doing this...
@@ -18,16 +19,18 @@ if sys.argv[1] in install_args and "--no-init-script" not in sys.argv:
                   ('/etc/init.d', ['install/initd/airtime-celery'])]
 else:
     if "--no-init-script" in sys.argv:
+        no_init = True
         run_postinst = True  # We still want to run the postinst here
         sys.argv.remove("--no-init-script")
     data_files = []
 
 
 def postinst():
-    # Make /etc/init.d file executable and set proper
-    # permissions for the defaults config file
-    os.chmod('/etc/init.d/airtime-celery', 0755)
-    os.chmod('/etc/default/airtime-celery', 0640)
+    if not no_init:
+        # Make /etc/init.d file executable and set proper
+        # permissions for the defaults config file
+        os.chmod('/etc/init.d/airtime-celery', 0755)
+        os.chmod('/etc/default/airtime-celery', 0640)
     # Make the airtime log directory group-writable
     os.chmod('/var/log/airtime', 0775)
 

From f26559c0c667523a5b0d6868989b1dbeb9ca8471 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 24 Jun 2015 14:48:38 -0400
Subject: [PATCH 27/39] Fix permissions for SoundCloud functionality

---
 airtime_mvc/application/configs/ACL.php       |  4 +-
 .../controllers/LibraryController.php         | 43 +++++++++++--------
 2 files changed, 27 insertions(+), 20 deletions(-)

diff --git a/airtime_mvc/application/configs/ACL.php b/airtime_mvc/application/configs/ACL.php
index 585227215..07ca7d822 100644
--- a/airtime_mvc/application/configs/ACL.php
+++ b/airtime_mvc/application/configs/ACL.php
@@ -59,8 +59,9 @@ $ccAcl->allow('G', 'index')
       ->allow('G', 'provisioning')
       ->allow('G', 'downgrade')
       ->allow('G', 'rest:show-image', 'get')
-      ->allow('H', 'rest:show-image')
       ->allow('G', 'rest:media', 'get')
+      ->allow('H', 'soundcloud')
+      ->allow('H', 'rest:show-image')
       ->allow('H', 'rest:media')
       ->allow('H', 'preference', 'is-import-in-progress')
       ->allow('H', 'usersettings')
@@ -74,7 +75,6 @@ $ccAcl->allow('G', 'index')
       ->allow('A', 'systemstatus')
       ->allow('A', 'preference')
       ->allow('A', 'player')
-      ->allow('A', 'soundcloud')
       ->allow('A', 'embeddablewidgets')
       ->allow('S', 'thank-you')
       ->allow('S', 'billing');
diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index e2de84adf..2ad032973 100644
--- a/airtime_mvc/application/controllers/LibraryController.php
+++ b/airtime_mvc/application/controllers/LibraryController.php
@@ -266,28 +266,35 @@ class LibraryController extends Zend_Controller_Action
         }
 
         // SOUNDCLOUD MENU OPTION
-        $soundcloudService = new SoundcloudService();
-        if ($type === "audioclip" && $soundcloudService->hasAccessToken()) {
+        $ownerId = empty($obj) ? $file->getFileOwnerId() : $obj->getCreatorId();
+        if ($isAdminOrPM || $ownerId == $user->getId()) {
+            $soundcloudService = new SoundcloudService();
+            if ($type === "audioclip" && $soundcloudService->hasAccessToken()) {
 
-            //create a menu separator
-            $menu["sep1"] = "-----------";
+                //create a menu separator
+                $menu["sep1"] = "-----------";
 
-            //create a sub menu for Soundcloud actions.
-            $menu["soundcloud"] = array("name" => _("Soundcloud"), "icon" => "soundcloud", "items" => array());
+                //create a sub menu for Soundcloud actions.
+                $menu["soundcloud"] = array("name" => _("Soundcloud"), "icon" => "soundcloud", "items" => array());
 
-            $serviceId = $soundcloudService->getServiceId($id);
-            if (!is_null($file) && $serviceId != 0) {
-                $menu["soundcloud"]["items"]["view"] = array("name" => _("View track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/view-on-sound-cloud/id/{$id}");
-                $menu["soundcloud"]["items"]["remove"] = array("name" => _("Remove track"), "icon" => "soundcloud", "url" => $baseUrl."soundcloud/delete/id/{$id}");
-            } else {
-                // If a reference exists for this file ID, that means the user has uploaded the track
-                // but we haven't yet gotten a response from Celery, so disable the menu item
-                if ($soundcloudService->referenceExists($id)) {
-                    $menu["soundcloud"]["items"]["upload"] = array("name" => _("Upload track"), "icon" => "soundcloud",
-                                                                   "url" => $baseUrl."soundcloud/upload/id/{$id}", "disabled" => true);
+                $serviceId = $soundcloudService->getServiceId($id);
+                if (!is_null($file) && $serviceId != 0) {
+                    $menu["soundcloud"]["items"]["view"] = array("name" => _("View track"), "icon" => "soundcloud", "url" => $baseUrl . "soundcloud/view-on-sound-cloud/id/{$id}");
+                    $menu["soundcloud"]["items"]["remove"] = array("name" => _("Remove track"), "icon" => "soundcloud", "url" => $baseUrl . "soundcloud/delete/id/{$id}");
                 } else {
-                    $menu["soundcloud"]["items"]["upload"] = array("name" => _("Upload track"), "icon" => "soundcloud",
-                                                                   "url" => $baseUrl."soundcloud/upload/id/{$id}");
+                    // If a reference exists for this file ID, that means the user has uploaded the track
+                    // but we haven't yet gotten a response from Celery, so disable the menu item
+                    if ($soundcloudService->referenceExists($id)) {
+                        $menu["soundcloud"]["items"]["upload"] = array(
+                            "name" => _("Upload track"), "icon" => "soundcloud",
+                            "url" => $baseUrl . "soundcloud/upload/id/{$id}", "disabled" => true
+                        );
+                    } else {
+                        $menu["soundcloud"]["items"]["upload"] = array(
+                            "name" => _("Upload track"), "icon" => "soundcloud",
+                            "url" => $baseUrl . "soundcloud/upload/id/{$id}"
+                        );
+                    }
                 }
             }
         }

From e44e0de5346cd6398e7b7151d77d627207e4ac72 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 24 Jun 2015 15:46:53 -0400
Subject: [PATCH 28/39] Add fields to SoundCloud upload

---
 airtime_mvc/application/models/StoredFile.php          | 3 +++
 airtime_mvc/application/services/SoundcloudService.php | 6 +++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php
index ac639b9bb..b14975b1d 100644
--- a/airtime_mvc/application/models/StoredFile.php
+++ b/airtime_mvc/application/models/StoredFile.php
@@ -73,6 +73,9 @@ class Application_Model_StoredFile
         return $this->_file->getDbFtype();
     }
 
+    /**
+     * @return CcFiles
+     */
     public function getPropelOrm()
     {
         return $this->_file;
diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index 4aac783a7..93e1e9041 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -68,8 +68,12 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
      * @return array the track array to send to SoundCloud
      */
     protected function _getUploadData($file) {
+        $file = $file->getPropelOrm();
+        // TODO: Move this into a proper serializer
         $trackArray = array(
-            'title' => $file->getName(),
+            'title' => $file->getDbName(),
+            'genre' => $file->getDbGenre(),
+            'bpm'   => $file->getDbBpm(),
         );
         foreach (self::$_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
             $val = Application_Model_Preference::$func();

From adff664e87477eabe9f209505fdd07bcbcd7faf1 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 24 Jun 2015 16:13:04 -0400
Subject: [PATCH 29/39] Fix sending null fields to SoundCloud

---
 .../services/SoundcloudService.php            | 32 ++++++++++++++++---
 1 file changed, 27 insertions(+), 5 deletions(-)

diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index 93e1e9041..dcb207f9e 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -69,12 +69,9 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
      */
     protected function _getUploadData($file) {
         $file = $file->getPropelOrm();
+        Logging::info($file);
         // TODO: Move this into a proper serializer
-        $trackArray = array(
-            'title' => $file->getDbName(),
-            'genre' => $file->getDbGenre(),
-            'bpm'   => $file->getDbBpm(),
-        );
+        $trackArray = $this->_serializeTrack($file);
         foreach (self::$_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
             $val = Application_Model_Preference::$func();
             if (!empty($val)) {
@@ -85,6 +82,31 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
         return $trackArray;
     }
 
+    /**
+     * Serialize Airtime file data to send to SoundCloud
+     *
+     * Ignores any null fields, as these will cause the upload to throw a 422
+     * Unprocessable Entity error
+     *
+     * @param $file CcFiles file object
+     *
+     * @return array the serialized data
+     */
+    protected function _serializeTrack($file) {
+        $fileData = array(
+            'title' => $file->getDbTrackTitle(),
+            'genre' => $file->getDbGenre(),
+            'bpm'   => $file->getDbBpm(),
+        );
+        $trackArray = array();
+        foreach ($fileData as $k => $v) {
+            if (!empty($v)) {
+                $trackArray[$k] = $v;
+            }
+        }
+        return $trackArray;
+    }
+
     /**
      * Update a ThirdPartyTrackReferences object for a completed upload
      * TODO: should we have a database layer class to handle Propel operations?

From 67155b136afc35ff9a162072c1f34b4205687e3e Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Wed, 24 Jun 2015 18:38:04 -0400
Subject: [PATCH 30/39] Add downgrade action to UpgradeController, fix
 SoundCloud schema and bugs

---
 .../controllers/UpgradeController.php         |  31 ++++
 .../airtime_2.5.13/downgrade.sql              |  11 ++
 .../upgrade_sql/airtime_2.5.13/upgrade.sql    |   3 +-
 .../airtime/map/CeleryTasksTableMap.php       |   6 +-
 .../models/airtime/om/BaseCeleryTasks.php     | 124 +++++++++++----
 .../models/airtime/om/BaseCeleryTasksPeer.php |  41 +++--
 .../airtime/om/BaseCeleryTasksQuery.php       |  70 +++++++--
 .../application/services/CeleryService.php    |   8 +-
 .../services/SoundcloudService.php            |   1 -
 .../services/ThirdPartyCeleryService.php      |   8 +-
 airtime_mvc/application/upgrade/Upgrades.php  | 142 +++++++++++++++---
 airtime_mvc/build/schema.xml                  |   3 +-
 airtime_mvc/build/sql/schema.sql              |   3 +-
 .../public/js/airtime/library/library.js      |   3 +-
 14 files changed, 361 insertions(+), 93 deletions(-)
 create mode 100644 airtime_mvc/application/controllers/downgrade_sql/airtime_2.5.13/downgrade.sql

diff --git a/airtime_mvc/application/controllers/UpgradeController.php b/airtime_mvc/application/controllers/UpgradeController.php
index c2bd2bfae..465ab9f2b 100644
--- a/airtime_mvc/application/controllers/UpgradeController.php
+++ b/airtime_mvc/application/controllers/UpgradeController.php
@@ -33,4 +33,35 @@ class UpgradeController extends Zend_Controller_Action
                  ->appendBody($e->getMessage());
         }
     }
+
+    public function downgradeAction() {
+        $this->view->layout()->disableLayout();
+        $this->_helper->viewRenderer->setNoRender(true);
+
+        if (!RestAuth::verifyAuth(true, false, $this)) {
+            return;
+        }
+
+        $request = $this->getRequest();
+        $toVersion = $request->getParam("version");
+
+        try {
+            $downgradePerformed = UpgradeManager::doDowngrade($toVersion);
+
+            if (!$downgradePerformed) {
+                $this->getResponse()
+                    ->setHttpResponseCode(200)
+                    ->appendBody("No downgrade was performed. The current schema version is " . Application_Model_Preference::GetSchemaVersion() . ".<br>");
+            } else {
+                $this->getResponse()
+                    ->setHttpResponseCode(200)
+                    ->appendBody("Downgrade to Airtime schema version " . Application_Model_Preference::GetSchemaVersion() . " OK<br>");
+            }
+        } catch (Exception $e) {
+            $this->getResponse()
+                ->setHttpResponseCode(400)
+                ->appendBody($e->getMessage());
+        }
+    }
+
 }
diff --git a/airtime_mvc/application/controllers/downgrade_sql/airtime_2.5.13/downgrade.sql b/airtime_mvc/application/controllers/downgrade_sql/airtime_2.5.13/downgrade.sql
new file mode 100644
index 000000000..5943649f2
--- /dev/null
+++ b/airtime_mvc/application/controllers/downgrade_sql/airtime_2.5.13/downgrade.sql
@@ -0,0 +1,11 @@
+-----------------------------------------------------------------------
+-- third_party_track_references
+-----------------------------------------------------------------------
+
+DROP TABLE IF EXISTS "third_party_track_references" CASCADE;
+
+-----------------------------------------------------------------------
+-- celery_tasks
+-----------------------------------------------------------------------
+
+DROP TABLE IF EXISTS "celery_tasks" CASCADE;
\ No newline at end of file
diff --git a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
index fbf7235bf..42ba8a8d0 100644
--- a/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
+++ b/airtime_mvc/application/controllers/upgrade_sql/airtime_2.5.13/upgrade.sql
@@ -20,7 +20,8 @@ CREATE TABLE IF NOT EXISTS "third_party_track_references"
 
 CREATE TABLE IF NOT EXISTS "celery_tasks"
 (
-    "id" VARCHAR(256) NOT NULL,
+    "id" serial NOT NULL,
+    "task_id" VARCHAR(256) NOT NULL,
     "track_reference" INTEGER NOT NULL,
     "name" VARCHAR(256),
     "dispatch_time" TIMESTAMP,
diff --git a/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php b/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php
index f2ed15cb6..042cee8d8 100644
--- a/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CeleryTasksTableMap.php
@@ -36,9 +36,11 @@ class CeleryTasksTableMap extends TableMap
         $this->setPhpName('CeleryTasks');
         $this->setClassname('CeleryTasks');
         $this->setPackage('airtime');
-        $this->setUseIdGenerator(false);
+        $this->setUseIdGenerator(true);
+        $this->setPrimaryKeyMethodInfo('celery_tasks_id_seq');
         // columns
-        $this->addPrimaryKey('id', 'DbId', 'VARCHAR', true, 256, null);
+        $this->addPrimaryKey('id', 'DbId', 'INTEGER', true, null, null);
+        $this->addColumn('task_id', 'DbTaskId', 'VARCHAR', true, 256, null);
         $this->addForeignKey('track_reference', 'DbTrackReference', 'INTEGER', 'third_party_track_references', 'id', true, null, null);
         $this->addColumn('name', 'DbName', 'VARCHAR', false, 256, null);
         $this->addColumn('dispatch_time', 'DbDispatchTime', 'TIMESTAMP', false, null, null);
diff --git a/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php b/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php
index c95a5f649..70879be46 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCeleryTasks.php
@@ -31,10 +31,16 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
 
     /**
      * The value for the id field.
-     * @var        string
+     * @var        int
      */
     protected $id;
 
+    /**
+     * The value for the task_id field.
+     * @var        string
+     */
+    protected $task_id;
+
     /**
      * The value for the track_reference field.
      * @var        int
@@ -87,7 +93,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
     /**
      * Get the [id] column value.
      *
-     * @return string
+     * @return int
      */
     public function getDbId()
     {
@@ -95,6 +101,17 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
         return $this->id;
     }
 
+    /**
+     * Get the [task_id] column value.
+     *
+     * @return string
+     */
+    public function getDbTaskId()
+    {
+
+        return $this->task_id;
+    }
+
     /**
      * Get the [track_reference] column value.
      *
@@ -166,13 +183,13 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
     /**
      * Set the value of [id] column.
      *
-     * @param  string $v new value
+     * @param  int $v new value
      * @return CeleryTasks The current object (for fluent API support)
      */
     public function setDbId($v)
     {
         if ($v !== null && is_numeric($v)) {
-            $v = (string) $v;
+            $v = (int) $v;
         }
 
         if ($this->id !== $v) {
@@ -184,6 +201,27 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
         return $this;
     } // setDbId()
 
+    /**
+     * Set the value of [task_id] column.
+     *
+     * @param  string $v new value
+     * @return CeleryTasks The current object (for fluent API support)
+     */
+    public function setDbTaskId($v)
+    {
+        if ($v !== null && is_numeric($v)) {
+            $v = (string) $v;
+        }
+
+        if ($this->task_id !== $v) {
+            $this->task_id = $v;
+            $this->modifiedColumns[] = CeleryTasksPeer::TASK_ID;
+        }
+
+
+        return $this;
+    } // setDbTaskId()
+
     /**
      * Set the value of [track_reference] column.
      *
@@ -306,11 +344,12 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
     {
         try {
 
-            $this->id = ($row[$startcol + 0] !== null) ? (string) $row[$startcol + 0] : null;
-            $this->track_reference = ($row[$startcol + 1] !== null) ? (int) $row[$startcol + 1] : null;
-            $this->name = ($row[$startcol + 2] !== null) ? (string) $row[$startcol + 2] : null;
-            $this->dispatch_time = ($row[$startcol + 3] !== null) ? (string) $row[$startcol + 3] : null;
-            $this->status = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->id = ($row[$startcol + 0] !== null) ? (int) $row[$startcol + 0] : null;
+            $this->task_id = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null;
+            $this->track_reference = ($row[$startcol + 2] !== null) ? (int) $row[$startcol + 2] : null;
+            $this->name = ($row[$startcol + 3] !== null) ? (string) $row[$startcol + 3] : null;
+            $this->dispatch_time = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null;
+            $this->status = ($row[$startcol + 5] !== null) ? (string) $row[$startcol + 5] : null;
             $this->resetModified();
 
             $this->setNew(false);
@@ -320,7 +359,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
             }
             $this->postHydrate($row, $startcol, $rehydrate);
 
-            return $startcol + 5; // 5 = CeleryTasksPeer::NUM_HYDRATE_COLUMNS.
+            return $startcol + 6; // 6 = CeleryTasksPeer::NUM_HYDRATE_COLUMNS.
 
         } catch (Exception $e) {
             throw new PropelException("Error populating CeleryTasks object", $e);
@@ -542,11 +581,28 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
         $modifiedColumns = array();
         $index = 0;
 
+        $this->modifiedColumns[] = CeleryTasksPeer::ID;
+        if (null !== $this->id) {
+            throw new PropelException('Cannot insert a value for auto-increment primary key (' . CeleryTasksPeer::ID . ')');
+        }
+        if (null === $this->id) {
+            try {
+                $stmt = $con->query("SELECT nextval('celery_tasks_id_seq')");
+                $row = $stmt->fetch(PDO::FETCH_NUM);
+                $this->id = $row[0];
+            } catch (Exception $e) {
+                throw new PropelException('Unable to get sequence id.', $e);
+            }
+        }
+
 
          // check the columns in natural order for more readable SQL queries
         if ($this->isColumnModified(CeleryTasksPeer::ID)) {
             $modifiedColumns[':p' . $index++]  = '"id"';
         }
+        if ($this->isColumnModified(CeleryTasksPeer::TASK_ID)) {
+            $modifiedColumns[':p' . $index++]  = '"task_id"';
+        }
         if ($this->isColumnModified(CeleryTasksPeer::TRACK_REFERENCE)) {
             $modifiedColumns[':p' . $index++]  = '"track_reference"';
         }
@@ -571,7 +627,10 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
             foreach ($modifiedColumns as $identifier => $columnName) {
                 switch ($columnName) {
                     case '"id"':
-                        $stmt->bindValue($identifier, $this->id, PDO::PARAM_STR);
+                        $stmt->bindValue($identifier, $this->id, PDO::PARAM_INT);
+                        break;
+                    case '"task_id"':
+                        $stmt->bindValue($identifier, $this->task_id, PDO::PARAM_STR);
                         break;
                     case '"track_reference"':
                         $stmt->bindValue($identifier, $this->track_reference, PDO::PARAM_INT);
@@ -728,15 +787,18 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
                 return $this->getDbId();
                 break;
             case 1:
-                return $this->getDbTrackReference();
+                return $this->getDbTaskId();
                 break;
             case 2:
-                return $this->getDbName();
+                return $this->getDbTrackReference();
                 break;
             case 3:
-                return $this->getDbDispatchTime();
+                return $this->getDbName();
                 break;
             case 4:
+                return $this->getDbDispatchTime();
+                break;
+            case 5:
                 return $this->getDbStatus();
                 break;
             default:
@@ -769,10 +831,11 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
         $keys = CeleryTasksPeer::getFieldNames($keyType);
         $result = array(
             $keys[0] => $this->getDbId(),
-            $keys[1] => $this->getDbTrackReference(),
-            $keys[2] => $this->getDbName(),
-            $keys[3] => $this->getDbDispatchTime(),
-            $keys[4] => $this->getDbStatus(),
+            $keys[1] => $this->getDbTaskId(),
+            $keys[2] => $this->getDbTrackReference(),
+            $keys[3] => $this->getDbName(),
+            $keys[4] => $this->getDbDispatchTime(),
+            $keys[5] => $this->getDbStatus(),
         );
         $virtualColumns = $this->virtualColumns;
         foreach ($virtualColumns as $key => $virtualColumn) {
@@ -821,15 +884,18 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
                 $this->setDbId($value);
                 break;
             case 1:
-                $this->setDbTrackReference($value);
+                $this->setDbTaskId($value);
                 break;
             case 2:
-                $this->setDbName($value);
+                $this->setDbTrackReference($value);
                 break;
             case 3:
-                $this->setDbDispatchTime($value);
+                $this->setDbName($value);
                 break;
             case 4:
+                $this->setDbDispatchTime($value);
+                break;
+            case 5:
                 $this->setDbStatus($value);
                 break;
         } // switch()
@@ -857,10 +923,11 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
         $keys = CeleryTasksPeer::getFieldNames($keyType);
 
         if (array_key_exists($keys[0], $arr)) $this->setDbId($arr[$keys[0]]);
-        if (array_key_exists($keys[1], $arr)) $this->setDbTrackReference($arr[$keys[1]]);
-        if (array_key_exists($keys[2], $arr)) $this->setDbName($arr[$keys[2]]);
-        if (array_key_exists($keys[3], $arr)) $this->setDbDispatchTime($arr[$keys[3]]);
-        if (array_key_exists($keys[4], $arr)) $this->setDbStatus($arr[$keys[4]]);
+        if (array_key_exists($keys[1], $arr)) $this->setDbTaskId($arr[$keys[1]]);
+        if (array_key_exists($keys[2], $arr)) $this->setDbTrackReference($arr[$keys[2]]);
+        if (array_key_exists($keys[3], $arr)) $this->setDbName($arr[$keys[3]]);
+        if (array_key_exists($keys[4], $arr)) $this->setDbDispatchTime($arr[$keys[4]]);
+        if (array_key_exists($keys[5], $arr)) $this->setDbStatus($arr[$keys[5]]);
     }
 
     /**
@@ -873,6 +940,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
         $criteria = new Criteria(CeleryTasksPeer::DATABASE_NAME);
 
         if ($this->isColumnModified(CeleryTasksPeer::ID)) $criteria->add(CeleryTasksPeer::ID, $this->id);
+        if ($this->isColumnModified(CeleryTasksPeer::TASK_ID)) $criteria->add(CeleryTasksPeer::TASK_ID, $this->task_id);
         if ($this->isColumnModified(CeleryTasksPeer::TRACK_REFERENCE)) $criteria->add(CeleryTasksPeer::TRACK_REFERENCE, $this->track_reference);
         if ($this->isColumnModified(CeleryTasksPeer::NAME)) $criteria->add(CeleryTasksPeer::NAME, $this->name);
         if ($this->isColumnModified(CeleryTasksPeer::DISPATCH_TIME)) $criteria->add(CeleryTasksPeer::DISPATCH_TIME, $this->dispatch_time);
@@ -899,7 +967,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
 
     /**
      * Returns the primary key for this object (row).
-     * @return string
+     * @return int
      */
     public function getPrimaryKey()
     {
@@ -909,7 +977,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
     /**
      * Generic method to set the primary key (id column).
      *
-     * @param  string $key Primary key.
+     * @param  int $key Primary key.
      * @return void
      */
     public function setPrimaryKey($key)
@@ -940,6 +1008,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
      */
     public function copyInto($copyObj, $deepCopy = false, $makeNew = true)
     {
+        $copyObj->setDbTaskId($this->getDbTaskId());
         $copyObj->setDbTrackReference($this->getDbTrackReference());
         $copyObj->setDbName($this->getDbName());
         $copyObj->setDbDispatchTime($this->getDbDispatchTime());
@@ -1060,6 +1129,7 @@ abstract class BaseCeleryTasks extends BaseObject implements Persistent
     public function clear()
     {
         $this->id = null;
+        $this->task_id = null;
         $this->track_reference = null;
         $this->name = null;
         $this->dispatch_time = null;
diff --git a/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php
index 2a651fd0e..b6dff77b4 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksPeer.php
@@ -24,17 +24,20 @@ abstract class BaseCeleryTasksPeer
     const TM_CLASS = 'CeleryTasksTableMap';
 
     /** The total number of columns. */
-    const NUM_COLUMNS = 5;
+    const NUM_COLUMNS = 6;
 
     /** The number of lazy-loaded columns. */
     const NUM_LAZY_LOAD_COLUMNS = 0;
 
     /** The number of columns to hydrate (NUM_COLUMNS - NUM_LAZY_LOAD_COLUMNS) */
-    const NUM_HYDRATE_COLUMNS = 5;
+    const NUM_HYDRATE_COLUMNS = 6;
 
     /** the column name for the id field */
     const ID = 'celery_tasks.id';
 
+    /** the column name for the task_id field */
+    const TASK_ID = 'celery_tasks.task_id';
+
     /** the column name for the track_reference field */
     const TRACK_REFERENCE = 'celery_tasks.track_reference';
 
@@ -66,12 +69,12 @@ abstract class BaseCeleryTasksPeer
      * e.g. CeleryTasksPeer::$fieldNames[CeleryTasksPeer::TYPE_PHPNAME][0] = 'Id'
      */
     protected static $fieldNames = array (
-        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbTrackReference', 'DbName', 'DbDispatchTime', 'DbStatus', ),
-        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbTrackReference', 'dbName', 'dbDispatchTime', 'dbStatus', ),
-        BasePeer::TYPE_COLNAME => array (CeleryTasksPeer::ID, CeleryTasksPeer::TRACK_REFERENCE, CeleryTasksPeer::NAME, CeleryTasksPeer::DISPATCH_TIME, CeleryTasksPeer::STATUS, ),
-        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'TRACK_REFERENCE', 'NAME', 'DISPATCH_TIME', 'STATUS', ),
-        BasePeer::TYPE_FIELDNAME => array ('id', 'track_reference', 'name', 'dispatch_time', 'status', ),
-        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+        BasePeer::TYPE_PHPNAME => array ('DbId', 'DbTaskId', 'DbTrackReference', 'DbName', 'DbDispatchTime', 'DbStatus', ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbTaskId', 'dbTrackReference', 'dbName', 'dbDispatchTime', 'dbStatus', ),
+        BasePeer::TYPE_COLNAME => array (CeleryTasksPeer::ID, CeleryTasksPeer::TASK_ID, CeleryTasksPeer::TRACK_REFERENCE, CeleryTasksPeer::NAME, CeleryTasksPeer::DISPATCH_TIME, CeleryTasksPeer::STATUS, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID', 'TASK_ID', 'TRACK_REFERENCE', 'NAME', 'DISPATCH_TIME', 'STATUS', ),
+        BasePeer::TYPE_FIELDNAME => array ('id', 'task_id', 'track_reference', 'name', 'dispatch_time', 'status', ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, )
     );
 
     /**
@@ -81,12 +84,12 @@ abstract class BaseCeleryTasksPeer
      * e.g. CeleryTasksPeer::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0
      */
     protected static $fieldKeys = array (
-        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbTrackReference' => 1, 'DbName' => 2, 'DbDispatchTime' => 3, 'DbStatus' => 4, ),
-        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbTrackReference' => 1, 'dbName' => 2, 'dbDispatchTime' => 3, 'dbStatus' => 4, ),
-        BasePeer::TYPE_COLNAME => array (CeleryTasksPeer::ID => 0, CeleryTasksPeer::TRACK_REFERENCE => 1, CeleryTasksPeer::NAME => 2, CeleryTasksPeer::DISPATCH_TIME => 3, CeleryTasksPeer::STATUS => 4, ),
-        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'TRACK_REFERENCE' => 1, 'NAME' => 2, 'DISPATCH_TIME' => 3, 'STATUS' => 4, ),
-        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'track_reference' => 1, 'name' => 2, 'dispatch_time' => 3, 'status' => 4, ),
-        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
+        BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbTaskId' => 1, 'DbTrackReference' => 2, 'DbName' => 3, 'DbDispatchTime' => 4, 'DbStatus' => 5, ),
+        BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbTaskId' => 1, 'dbTrackReference' => 2, 'dbName' => 3, 'dbDispatchTime' => 4, 'dbStatus' => 5, ),
+        BasePeer::TYPE_COLNAME => array (CeleryTasksPeer::ID => 0, CeleryTasksPeer::TASK_ID => 1, CeleryTasksPeer::TRACK_REFERENCE => 2, CeleryTasksPeer::NAME => 3, CeleryTasksPeer::DISPATCH_TIME => 4, CeleryTasksPeer::STATUS => 5, ),
+        BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'TASK_ID' => 1, 'TRACK_REFERENCE' => 2, 'NAME' => 3, 'DISPATCH_TIME' => 4, 'STATUS' => 5, ),
+        BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'task_id' => 1, 'track_reference' => 2, 'name' => 3, 'dispatch_time' => 4, 'status' => 5, ),
+        BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, )
     );
 
     /**
@@ -161,12 +164,14 @@ abstract class BaseCeleryTasksPeer
     {
         if (null === $alias) {
             $criteria->addSelectColumn(CeleryTasksPeer::ID);
+            $criteria->addSelectColumn(CeleryTasksPeer::TASK_ID);
             $criteria->addSelectColumn(CeleryTasksPeer::TRACK_REFERENCE);
             $criteria->addSelectColumn(CeleryTasksPeer::NAME);
             $criteria->addSelectColumn(CeleryTasksPeer::DISPATCH_TIME);
             $criteria->addSelectColumn(CeleryTasksPeer::STATUS);
         } else {
             $criteria->addSelectColumn($alias . '.id');
+            $criteria->addSelectColumn($alias . '.task_id');
             $criteria->addSelectColumn($alias . '.track_reference');
             $criteria->addSelectColumn($alias . '.name');
             $criteria->addSelectColumn($alias . '.dispatch_time');
@@ -409,7 +414,7 @@ abstract class BaseCeleryTasksPeer
     public static function getPrimaryKeyFromRow($row, $startcol = 0)
     {
 
-        return (string) $row[$startcol];
+        return (int) $row[$startcol];
     }
 
     /**
@@ -764,6 +769,10 @@ abstract class BaseCeleryTasksPeer
             $criteria = $values->buildCriteria(); // build Criteria from CeleryTasks object
         }
 
+        if ($criteria->containsKey(CeleryTasksPeer::ID) && $criteria->keyContainsValue(CeleryTasksPeer::ID) ) {
+            throw new PropelException('Cannot insert a value for auto-increment primary key ('.CeleryTasksPeer::ID.')');
+        }
+
 
         // Set the correct dbName
         $criteria->setDbName(CeleryTasksPeer::DATABASE_NAME);
@@ -952,7 +961,7 @@ abstract class BaseCeleryTasksPeer
     /**
      * Retrieve a single object by pkey.
      *
-     * @param string $pk the primary key.
+     * @param int $pk the primary key.
      * @param      PropelPDO $con the connection to use
      * @return CeleryTasks
      */
diff --git a/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php
index 20e683d70..9d25080d0 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCeleryTasksQuery.php
@@ -7,12 +7,14 @@
  *
  *
  * @method CeleryTasksQuery orderByDbId($order = Criteria::ASC) Order by the id column
+ * @method CeleryTasksQuery orderByDbTaskId($order = Criteria::ASC) Order by the task_id column
  * @method CeleryTasksQuery orderByDbTrackReference($order = Criteria::ASC) Order by the track_reference column
  * @method CeleryTasksQuery orderByDbName($order = Criteria::ASC) Order by the name column
  * @method CeleryTasksQuery orderByDbDispatchTime($order = Criteria::ASC) Order by the dispatch_time column
  * @method CeleryTasksQuery orderByDbStatus($order = Criteria::ASC) Order by the status column
  *
  * @method CeleryTasksQuery groupByDbId() Group by the id column
+ * @method CeleryTasksQuery groupByDbTaskId() Group by the task_id column
  * @method CeleryTasksQuery groupByDbTrackReference() Group by the track_reference column
  * @method CeleryTasksQuery groupByDbName() Group by the name column
  * @method CeleryTasksQuery groupByDbDispatchTime() Group by the dispatch_time column
@@ -29,12 +31,14 @@
  * @method CeleryTasks findOne(PropelPDO $con = null) Return the first CeleryTasks matching the query
  * @method CeleryTasks findOneOrCreate(PropelPDO $con = null) Return the first CeleryTasks matching the query, or a new CeleryTasks object populated from the query conditions when no match is found
  *
+ * @method CeleryTasks findOneByDbTaskId(string $task_id) Return the first CeleryTasks filtered by the task_id column
  * @method CeleryTasks findOneByDbTrackReference(int $track_reference) Return the first CeleryTasks filtered by the track_reference column
  * @method CeleryTasks findOneByDbName(string $name) Return the first CeleryTasks filtered by the name column
  * @method CeleryTasks findOneByDbDispatchTime(string $dispatch_time) Return the first CeleryTasks filtered by the dispatch_time column
  * @method CeleryTasks findOneByDbStatus(string $status) Return the first CeleryTasks filtered by the status column
  *
- * @method array findByDbId(string $id) Return CeleryTasks objects filtered by the id column
+ * @method array findByDbId(int $id) Return CeleryTasks objects filtered by the id column
+ * @method array findByDbTaskId(string $task_id) Return CeleryTasks objects filtered by the task_id column
  * @method array findByDbTrackReference(int $track_reference) Return CeleryTasks objects filtered by the track_reference column
  * @method array findByDbName(string $name) Return CeleryTasks objects filtered by the name column
  * @method array findByDbDispatchTime(string $dispatch_time) Return CeleryTasks objects filtered by the dispatch_time column
@@ -146,10 +150,10 @@ abstract class BaseCeleryTasksQuery extends ModelCriteria
      */
     protected function findPkSimple($key, $con)
     {
-        $sql = 'SELECT "id", "track_reference", "name", "dispatch_time", "status" FROM "celery_tasks" WHERE "id" = :p0';
+        $sql = 'SELECT "id", "task_id", "track_reference", "name", "dispatch_time", "status" FROM "celery_tasks" WHERE "id" = :p0';
         try {
             $stmt = $con->prepare($sql);
-            $stmt->bindValue(':p0', $key, PDO::PARAM_STR);
+            $stmt->bindValue(':p0', $key, PDO::PARAM_INT);
             $stmt->execute();
         } catch (Exception $e) {
             Propel::log($e->getMessage(), Propel::LOG_ERR);
@@ -240,30 +244,72 @@ abstract class BaseCeleryTasksQuery extends ModelCriteria
      *
      * Example usage:
      * <code>
-     * $query->filterByDbId('fooValue');   // WHERE id = 'fooValue'
-     * $query->filterByDbId('%fooValue%'); // WHERE id LIKE '%fooValue%'
+     * $query->filterByDbId(1234); // WHERE id = 1234
+     * $query->filterByDbId(array(12, 34)); // WHERE id IN (12, 34)
+     * $query->filterByDbId(array('min' => 12)); // WHERE id >= 12
+     * $query->filterByDbId(array('max' => 12)); // WHERE id <= 12
      * </code>
      *
-     * @param     string $dbId The value to use as filter.
-     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     mixed $dbId The value to use as filter.
+     *              Use scalar values for equality.
+     *              Use array values for in_array() equivalent.
+     *              Use associative array('min' => $minValue, 'max' => $maxValue) for intervals.
      * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
      *
      * @return CeleryTasksQuery The current query, for fluid interface
      */
     public function filterByDbId($dbId = null, $comparison = null)
     {
-        if (null === $comparison) {
-            if (is_array($dbId)) {
+        if (is_array($dbId)) {
+            $useMinMax = false;
+            if (isset($dbId['min'])) {
+                $this->addUsingAlias(CeleryTasksPeer::ID, $dbId['min'], Criteria::GREATER_EQUAL);
+                $useMinMax = true;
+            }
+            if (isset($dbId['max'])) {
+                $this->addUsingAlias(CeleryTasksPeer::ID, $dbId['max'], Criteria::LESS_EQUAL);
+                $useMinMax = true;
+            }
+            if ($useMinMax) {
+                return $this;
+            }
+            if (null === $comparison) {
                 $comparison = Criteria::IN;
-            } elseif (preg_match('/[\%\*]/', $dbId)) {
-                $dbId = str_replace('*', '%', $dbId);
-                $comparison = Criteria::LIKE;
             }
         }
 
         return $this->addUsingAlias(CeleryTasksPeer::ID, $dbId, $comparison);
     }
 
+    /**
+     * Filter the query on the task_id column
+     *
+     * Example usage:
+     * <code>
+     * $query->filterByDbTaskId('fooValue');   // WHERE task_id = 'fooValue'
+     * $query->filterByDbTaskId('%fooValue%'); // WHERE task_id LIKE '%fooValue%'
+     * </code>
+     *
+     * @param     string $dbTaskId The value to use as filter.
+     *              Accepts wildcards (* and % trigger a LIKE)
+     * @param     string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
+     *
+     * @return CeleryTasksQuery The current query, for fluid interface
+     */
+    public function filterByDbTaskId($dbTaskId = null, $comparison = null)
+    {
+        if (null === $comparison) {
+            if (is_array($dbTaskId)) {
+                $comparison = Criteria::IN;
+            } elseif (preg_match('/[\%\*]/', $dbTaskId)) {
+                $dbTaskId = str_replace('*', '%', $dbTaskId);
+                $comparison = Criteria::LIKE;
+            }
+        }
+
+        return $this->addUsingAlias(CeleryTasksPeer::TASK_ID, $dbTaskId, $comparison);
+    }
+
     /**
      * Filter the query on the track_reference column
      *
diff --git a/airtime_mvc/application/services/CeleryService.php b/airtime_mvc/application/services/CeleryService.php
index ad18a56b1..8e9091290 100644
--- a/airtime_mvc/application/services/CeleryService.php
+++ b/airtime_mvc/application/services/CeleryService.php
@@ -75,7 +75,7 @@ class CeleryService {
         $config = parse_ini_file(Application_Model_RabbitMq::getRmqConfigPath(), true);
         $queue = self::$_CELERY_RESULTS_EXCHANGE . "." . $task;
         $c = self::_setupCeleryExchange($config, self::$_CELERY_RESULTS_EXCHANGE, $queue);
-        $message = $c->getAsyncResultMessage($task->getDbName(), $task->getDbId());
+        $message = $c->getAsyncResultMessage($task->getDbName(), $task->getDbTaskId());
 
         // If the message isn't ready yet (Celery hasn't finished the task),
         // only throw an exception if the message has timed out.
@@ -85,12 +85,12 @@ class CeleryService {
                 // track reference here in case it was a deletion that failed, for example.
                 $task->setDbStatus(CELERY_FAILED_STATUS)->save();
                 throw new CeleryTimeoutException("Celery task " . $task->getDbName()
-                                                 . " with ID " . $task->getDbId() . " timed out");
+                                                 . " with ID " . $task->getDbTaskId() . " timed out");
             } else {
                 // The message hasn't timed out, but it's still false, which means it hasn't been
                 // sent back from Celery yet.
                 throw new CeleryException("Waiting on Celery task " . $task->getDbName()
-                                          . " with ID " . $task->getDbId());
+                                          . " with ID " . $task->getDbTaskId());
             }
         }
         return $message;
@@ -147,7 +147,7 @@ class CeleryService {
     protected static function _getPendingTasks($taskName, $serviceName) {
         $query = CeleryTasksQuery::create()
             ->filterByDbStatus(CELERY_PENDING_STATUS)
-            ->filterByDbId('', Criteria::NOT_EQUAL);
+            ->filterByDbTaskId('', Criteria::NOT_EQUAL);
         if (!empty($taskName)) {
             $query->filterByDbName($taskName);
         }
diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index dcb207f9e..494eeaf29 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -69,7 +69,6 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
      */
     protected function _getUploadData($file) {
         $file = $file->getPropelOrm();
-        Logging::info($file);
         // TODO: Move this into a proper serializer
         $trackArray = $this->_serializeTrack($file);
         foreach (self::$_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
diff --git a/airtime_mvc/application/services/ThirdPartyCeleryService.php b/airtime_mvc/application/services/ThirdPartyCeleryService.php
index 33d661e9b..5dbc1ebad 100644
--- a/airtime_mvc/application/services/ThirdPartyCeleryService.php
+++ b/airtime_mvc/application/services/ThirdPartyCeleryService.php
@@ -82,12 +82,8 @@ abstract class ThirdPartyCeleryService extends ThirdPartyService {
      */
     protected function _createTaskReference($fileId, $brokerTaskId, $taskName) {
         $trackId = $this->createTrackReference($fileId);
-        // First, check if the track already has an entry in the database
-        $task = CeleryTasksQuery::create()->findOneByDbTrackReference($trackId);
-        if (is_null($task)) {
-            $task = new CeleryTasks();
-        }
-        $task->setDbId($brokerTaskId);
+        $task = new CeleryTasks();
+        $task->setDbTaskId($brokerTaskId);
         $task->setDbName($taskName);
         $utc = new DateTimeZone("UTC");
         $task->setDbDispatchTime(new DateTime("now", $utc));
diff --git a/airtime_mvc/application/upgrade/Upgrades.php b/airtime_mvc/application/upgrade/Upgrades.php
index 7510e6c52..aa012eca6 100644
--- a/airtime_mvc/application/upgrade/Upgrades.php
+++ b/airtime_mvc/application/upgrade/Upgrades.php
@@ -22,7 +22,9 @@ function getUpgrades() {
 
 class UpgradeManager
 {
-    /** Used to determine if the database schema needs an upgrade in order for this version of the Airtime codebase to work correctly.
+
+    /**
+     * Used to determine if the database schema needs an upgrade in order for this version of the Airtime codebase to work correctly.
      * @return array A list of schema versions that this version of the codebase supports.
      */
     public static function getSupportedSchemaVersions()
@@ -64,10 +66,35 @@ class UpgradeManager
         return $upgradePerformed;
     }
 
+    /**
+     * Downgrade the Airtime schema version to match the given version
+     *
+     * @param string $toVersion the version we want to downgrade to
+     *
+     * @return boolean whether or not an upgrade was performed
+     */
+    public static function doDowngrade($toVersion)
+    {
+        $downgraders = array_reverse(getUpgrades());  // Reverse the array because we're downgrading
+        $dir = (dirname(__DIR__) . "/controllers");
+        $downgradePerformed = false;
+
+        foreach ($downgraders as $downgrader) {
+            /** @var AirtimeUpgrader $downgrader */
+            $downgrader = new $downgrader($dir);
+            if ($downgrader->getNewVersion() == $toVersion) {
+                break;  // We've reached the version we wanted to downgrade to, so break
+            }
+            $downgradePerformed = self::_runDowngrade($downgrader) ? true : $downgradePerformed;
+        }
+
+        return $downgradePerformed;
+    }
+
     /**
      * Run the given upgrade
      *
-     * @param $upgrader AirtimeUpgrader the upgrade class to be executed
+     * @param $upgrader AirtimeUpgrader the upgrader class to be executed
      *
      * @return bool true if the upgrade was successful, otherwise false
      */
@@ -75,12 +102,26 @@ class UpgradeManager
         return $upgrader->checkIfUpgradeSupported() && $upgrader->upgrade();
     }
 
+    /**
+     * Run the given downgrade
+     *
+     * @param $downgrader           AirtimeUpgrader the upgrader class to be executed
+     * @param $supportedVersions    array           array of supported versions
+     *
+     * @return bool true if the downgrade was successful, otherwise false
+     */
+    private static function _runDowngrade(AirtimeUpgrader $downgrader) {
+        return $downgrader->checkIfDowngradeSupported() && $downgrader->downgrade();
+    }
+
 }
 
 abstract class AirtimeUpgrader
 {
     protected $_dir;
 
+    protected $username, $password, $host, $database;
+
     /**
      * @param $dir string directory housing upgrade files
      */
@@ -105,7 +146,17 @@ abstract class AirtimeUpgrader
      */
     public function checkIfUpgradeSupported()
     {
-        return in_array(AirtimeUpgrader::getCurrentSchemaVersion(), $this->getSupportedSchemaVersions());
+        return in_array(static::getCurrentSchemaVersion(), $this->getSupportedSchemaVersions());
+    }
+
+    /**
+     * This function checks to see if this class can perform a downgrade of your version of Airtime
+     *
+     * @return boolean True if we can downgrade your version of Airtime.
+     */
+    public function checkIfDowngradeSupported()
+    {
+        return static::getCurrentSchemaVersion() == $this->getNewVersion();
     }
 
     protected function toggleMaintenanceScreen($toggle)
@@ -143,6 +194,7 @@ abstract class AirtimeUpgrader
             // $this->toggleMaintenanceScreen(true);
             Cache::clear();
 
+            $this->_getDbValues();
             $this->_runUpgrade();
 
             Application_Model_Preference::SetSchemaVersion($this->getNewVersion());
@@ -157,18 +209,57 @@ abstract class AirtimeUpgrader
         return true;
     }
 
-    protected function _runUpgrade() {
+    /**
+     * Implement this for each new version of Airtime
+     * This function abstracts out the core downgrade functionality,
+     * allowing child classes to overwrite _runDowngrade to reduce duplication
+     */
+    public function downgrade() {
+        Cache::clear();
+
+        try {
+            $this->_getDbValues();
+            $this->_runDowngrade();
+
+            $highestSupportedVersion = null;
+            foreach ($this->getSupportedSchemaVersions() as $v) {
+                // version_compare returns 1 (true) if the second parameter is lower
+                if (!$highestSupportedVersion || version_compare($v, $highestSupportedVersion)) {
+                    $highestSupportedVersion = $v;
+                }
+            }
+
+            // Set the schema version to the highest supported version so we don't skip versions when downgrading
+            Application_Model_Preference::SetSchemaVersion($highestSupportedVersion);
+
+            Cache::clear();
+        } catch(Exception $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function _getDbValues() {
         $airtimeConf = isset($_SERVER['AIRTIME_CONF']) ? $_SERVER['AIRTIME_CONF'] : "/etc/airtime/airtime.conf";
         $values = parse_ini_file($airtimeConf, true);
 
-        $username = $values['database']['dbuser'];
-        $password = $values['database']['dbpass'];
-        $host = $values['database']['host'];
-        $database = $values['database']['dbname'];
-
-        passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f ".$this->_dir."/upgrade_sql/airtime_"
-                 .$this->getNewVersion()."/upgrade.sql $database 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
+        $this->username = $values['database']['dbuser'];
+        $this->password = $values['database']['dbpass'];
+        $this->host     = $values['database']['host'];
+        $this->database = $values['database']['dbname'];
     }
+
+    protected function _runUpgrade() {
+        passthru("export PGPASSWORD=".$this->password." && psql -h ".$this->host." -U ".$this->username." -q -f ".$this->_dir."/upgrade_sql/airtime_"
+                 .$this->getNewVersion()."/upgrade.sql ".$this->database." 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
+    }
+
+    protected function _runDowngrade() {
+        passthru("export PGPASSWORD=".$this->password." && psql -h ".$this->host." -U ".$this->username." -q -f ".$this->_dir."/downgrade_sql/airtime_"
+                 .$this->getNewVersion()."/downgrade.sql ".$this->database." 2>&1 | grep -v -E \"will create implicit sequence|will create implicit index\"");
+    }
+
 }
 
 class AirtimeUpgrader253 extends AirtimeUpgrader
@@ -330,18 +421,27 @@ class AirtimeUpgrader2512 extends AirtimeUpgrader
 /**
  * Class AirtimeUpgrader2513 - Celery and SoundCloud upgrade
  *
- * Adds third_party_track_references table for third party service
+ * Adds third_party_track_references and celery_tasks tables for third party service
  * authentication and task architecture.
  *
- * Schema:
- *      id                          -> int          PK
- *      service                     -> string       internal service name
- *      foreign_id                  -> int          external unique service id
- *      broker_task_id              -> int          external unique amqp results identifier
- *      broker_task_name            -> string       external Celery task name
- *      broker_task_dispatch_time   -> timestamp    internal message dispatch time
- *      file_id                     -> int          internal FK->cc_files track id
- *      status                      -> string       external Celery task status - PENDING, SUCCESS, or FAILED
+ * <br/><b>third_party_track_references</b> schema:
+ *
+ *      id              -> int          PK
+ *      service         -> string       internal service name
+ *      foreign_id      -> int          external unique service id
+ *      file_id         -> int          internal FK->cc_files track id
+ *      upload_time     -> timestamp    internal upload timestamp
+ *      status          -> string       external service status
+ *
+ * <br/><b>celery_tasks</b> schema:
+ *
+ *      id              -> int          PK
+ *      task_id         -> string       external unique amqp results identifier
+ *      track_reference -> int          internal FK->third_party_track_references id
+ *      name            -> string       external Celery task name
+ *      dispatch_time   -> timestamp    internal message dispatch time
+ *      status          -> string       external Celery task status
+ *
  */
 class AirtimeUpgrader2513 extends AirtimeUpgrader
 {
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index a4ce8193b..87f820cb6 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -548,7 +548,8 @@
   </table>
 
   <table name="celery_tasks" phpName="CeleryTasks">
-    <column name="id" phpName="DbId" primaryKey="true" type="VARCHAR" size="256" required="true" />
+    <column name="id" phpName="DbId" primaryKey="true" type="INTEGER" autoIncrement="true" required="true" />
+    <column name="task_id" phpName="DbTaskId" type="VARCHAR" size="256" required="true" />
     <column name="track_reference" phpName="DbTrackReference" type="INTEGER" required="true" />
     <column name="name" phpName="DbName" type="VARCHAR" size="256" />
     <column name="dispatch_time" phpName="DbDispatchTime" type="TIMESTAMP" />
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index 9868de899..d358b5912 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -696,7 +696,8 @@ DROP TABLE IF EXISTS "celery_tasks" CASCADE;
 
 CREATE TABLE "celery_tasks"
 (
-    "id" VARCHAR(256) NOT NULL,
+    "id" serial NOT NULL,
+    "task_id" VARCHAR(256) NOT NULL,
     "track_reference" INTEGER NOT NULL,
     "name" VARCHAR(256),
     "dispatch_time" TIMESTAMP,
diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js
index 65bbb1111..562a25cbb 100644
--- a/airtime_mvc/public/js/airtime/library/library.js
+++ b/airtime_mvc/public/js/airtime/library/library.js
@@ -1018,7 +1018,8 @@ var AIRTIME = (function(AIRTIME) {
                         if (soundcloud.upload !== undefined) {
 
                             callback = function() {
-                                alert($.i18n._("Your track is being uploaded to SoundCloud"));
+                                alert($.i18n._("Your track is being uploaded and will " +
+                                               "appear on SoundCloud in a couple of minutes"));
                                 $.post(soundcloud.upload.url, function(){});
                             };
                             soundcloud.upload.callback = callback;

From d91b05a0aec211ddb03ea21d8e30690d2d9d4953 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Thu, 25 Jun 2015 10:53:35 -0400
Subject: [PATCH 31/39] SoundCloud - Add release_year metadata field and set
 default sharing and license types

---
 airtime_mvc/application/configs/constants.php         |  4 ++++
 .../application/forms/SoundCloudPreferences.php       |  2 +-
 airtime_mvc/application/models/Preference.php         |  8 ++++++--
 .../application/services/SoundcloudService.php        | 11 +++++++----
 4 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php
index 4b6ff1695..f498e8628 100644
--- a/airtime_mvc/application/configs/constants.php
+++ b/airtime_mvc/application/configs/constants.php
@@ -97,6 +97,10 @@ define('PROVISIONING_STATUS_ACTIVE' , 'Active');
 //TuneIn integration
 define("TUNEIN_API_URL", "http://air.radiotime.com/Playing.ashx");
 
+// SoundCloud
+define('DEFAULT_SOUNDCLOUD_LICENSE_TYPE', 'all-rights-reserved');
+define('DEFAULT_SOUNDCLOUD_SHARING_TYPE', 'public');
+
 // Celery
 define('CELERY_PENDING_STATUS', 'PENDING');
 define('CELERY_SUCCESS_STATUS', 'SUCCESS');
diff --git a/airtime_mvc/application/forms/SoundCloudPreferences.php b/airtime_mvc/application/forms/SoundCloudPreferences.php
index 591e811e8..987244ced 100644
--- a/airtime_mvc/application/forms/SoundCloudPreferences.php
+++ b/airtime_mvc/application/forms/SoundCloudPreferences.php
@@ -14,8 +14,8 @@ class Application_Form_SoundcloudPreferences extends Zend_Form_SubForm
         $select->setLabel(_('Default License:'));
         $select->setAttrib('class', 'input_select');
         $select->setMultiOptions(array(
-                                     "no-rights-reserved" => _("The work is in the public domain"),
                                      "all-rights-reserved" => _("All rights are reserved"),
+                                     "no-rights-reserved" => _("The work is in the public domain"),
                                      "cc-by" => _("Creative Commons Attribution"),
                                      "cc-by-nc" => _("Creative Commons Attribution Noncommercial"),
                                      "cc-by-nd" => _("Creative Commons Attribution No Derivative Works"),
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index bc5746b8e..cbcd0491e 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -1452,7 +1452,9 @@ class Application_Model_Preference
     // SoundCloud
 
     public static function getDefaultSoundCloudLicenseType() {
-        return self::getValue("soundcloud_license_type");
+        $val = self::getValue("soundcloud_license_type");
+        // If we don't have a value set, return all-rights-reserved by default
+        return empty($val) ? DEFAULT_SOUNDCLOUD_LICENSE_TYPE : $val;
     }
 
     public static function setDefaultSoundCloudLicenseType($value) {
@@ -1460,7 +1462,9 @@ class Application_Model_Preference
     }
 
     public static function getDefaultSoundCloudSharingType() {
-        return self::getValue("soundcloud_sharing_type");
+        $val = self::getValue("soundcloud_sharing_type");
+        // If we don't have a value set, return public by default
+        return empty($val) ? DEFAULT_SOUNDCLOUD_SHARING_TYPE : $val;
     }
 
     public static function setDefaultSoundCloudSharingType($value) {
diff --git a/airtime_mvc/application/services/SoundcloudService.php b/airtime_mvc/application/services/SoundcloudService.php
index 494eeaf29..8e946375e 100644
--- a/airtime_mvc/application/services/SoundcloudService.php
+++ b/airtime_mvc/application/services/SoundcloudService.php
@@ -69,7 +69,6 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
      */
     protected function _getUploadData($file) {
         $file = $file->getPropelOrm();
-        // TODO: Move this into a proper serializer
         $trackArray = $this->_serializeTrack($file);
         foreach (self::$_SOUNDCLOUD_PREF_FUNCTIONS as $func => $param) {
             $val = Application_Model_Preference::$func();
@@ -87,15 +86,18 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
      * Ignores any null fields, as these will cause the upload to throw a 422
      * Unprocessable Entity error
      *
+     * TODO: Move this into a proper serializer
+     *
      * @param $file CcFiles file object
      *
      * @return array the serialized data
      */
     protected function _serializeTrack($file) {
         $fileData = array(
-            'title' => $file->getDbTrackTitle(),
-            'genre' => $file->getDbGenre(),
-            'bpm'   => $file->getDbBpm(),
+            'title'         => $file->getDbTrackTitle(),
+            'genre'         => $file->getDbGenre(),
+            'bpm'           => $file->getDbBpm(),
+            'release_year'  => $file->getDbYear(),
         );
         $trackArray = array();
         foreach ($fileData as $k => $v) {
@@ -108,6 +110,7 @@ class SoundcloudService extends ThirdPartyCeleryService implements OAuth2 {
 
     /**
      * Update a ThirdPartyTrackReferences object for a completed upload
+     *
      * TODO: should we have a database layer class to handle Propel operations?
      *
      * @param $trackId int    ThirdPartyTrackReferences identifier

From ca9c805ce4ccbee153e2de4a0841019b9da154c3 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Fri, 26 Jun 2015 15:25:58 -0400
Subject: [PATCH 32/39] SAAS-906 - Change wording on Add to Show button

---
 .../public/js/airtime/library/events/library_showbuilder.js    | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js
index 315caf986..9674176f4 100644
--- a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js
+++ b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js
@@ -10,7 +10,8 @@ var AIRTIME = (function(AIRTIME) {
     mod.checkAddButton = function() {
         var selected = mod.getChosenItemsLength(), $cursor = $('tr.sb-selected'), check = false,
             shows = $('tr.sb-header'), current = $('tr.sb-current-show'),
-            cursorText = $.i18n._('Add to next show');
+        // TODO: this is an ugly way of doing this... we should find a more robust way of checking which view we're in.
+            cursorText = (window.location.href.toLowerCase().includes("schedule")) ? $.i18n._('Add to show') : $.i18n._('Add to next show');
 
         // make sure library items are selected and a cursor is selected.
         if (selected !== 0) {

From d9f17f3919c72c55a7bfd5f52ed4cbeadb27ae61 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Fri, 26 Jun 2015 17:11:04 -0400
Subject: [PATCH 33/39] SAAS-871 - Fix consumer cancellation bug

---
 python_apps/pypo/pypo/pypomessagehandler.py | 61 +++++++--------------
 1 file changed, 20 insertions(+), 41 deletions(-)

diff --git a/python_apps/pypo/pypo/pypomessagehandler.py b/python_apps/pypo/pypo/pypomessagehandler.py
index 8fdb43d9a..043cc6984 100644
--- a/python_apps/pypo/pypo/pypomessagehandler.py
+++ b/python_apps/pypo/pypo/pypomessagehandler.py
@@ -10,7 +10,7 @@ import time
 from kombu.connection import BrokerConnection
 from kombu.messaging import Exchange, Queue
 from kombu.simple import SimpleQueue
-from amqplib.client_0_8.exceptions import AMQPConnectionException
+from amqp.exceptions import AMQPError
 import json
 
 from std_err_override import LogWriter
@@ -29,21 +29,21 @@ class PypoMessageHandler(Thread):
 
     def init_rabbit_mq(self):
         self.logger.info("Initializing RabbitMQ stuff")
+        simple_queue = None
         try:
             schedule_exchange = Exchange("airtime-pypo", "direct", durable=True, auto_delete=True)
             schedule_queue = Queue("pypo-fetch", exchange=schedule_exchange, key="foo")
-            connection = BrokerConnection(self.config["host"], \
-                    self.config["user"], \
-                    self.config["password"], \
-                    self.config["vhost"])
+            connection = BrokerConnection(self.config["host"],
+                                          self.config["user"],
+                                          self.config["password"],
+                                          self.config["vhost"])
 
             channel = connection.channel()
-            self.simple_queue = SimpleQueue(channel, schedule_queue)
+            simple_queue = SimpleQueue(channel, schedule_queue)
         except Exception, e:
             self.logger.error(e)
-            return False
 
-        return True
+        return simple_queue
 
     """
     Handle a message from RabbitMQ, put it into our yucky global var.
@@ -91,39 +91,18 @@ class PypoMessageHandler(Thread):
             self.logger.error("Exception in handling RabbitMQ message: %s", e)
 
     def main(self):
-        while not self.init_rabbit_mq():
-            self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
-            time.sleep(5)
-
-        loops = 1
-        while True:
-            self.logger.info("Loop #%s", loops)
-            try:
-                message = self.simple_queue.get(block=True)
-                self.handle_message(message.payload)
-                # ACK the message to take it off the queue
-                message.ack()
-            except (IOError, AttributeError, AMQPConnectionException), e:
-                self.logger.error('Exception: %s', e)
-                self.logger.error("traceback: %s", traceback.format_exc())
-                while not self.init_rabbit_mq():
-                    self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
-                    time.sleep(5)
-            except Exception, e:
-                """
-                sleep 5 seconds so that we don't spin inside this
-                while loop and eat all the CPU
-                """
-                time.sleep(5)
-
-                """
-                There is a problem with the RabbitMq messenger service. Let's
-                log the error and get the schedule via HTTP polling
-                """
-                self.logger.error('Exception: %s', e)
-                self.logger.error("traceback: %s", traceback.format_exc())
-
-            loops += 1
+        try:
+            with self.init_rabbit_mq() as queue:
+                while True:
+                    message = queue.get(block=True)
+                    self.handle_message(message.payload)
+                    # ACK the message to take it off the queue
+                    message.ack()
+        except Exception, e:
+            self.logger.error('Exception: %s', e)
+            self.logger.error("traceback: %s", traceback.format_exc())
+        self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
+        time.sleep(5)
 
     """
     Main loop of the thread:

From 4be34ea94b7bb08fd7176e1d2850cd42fd53bc36 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 29 Jun 2015 10:26:52 -0400
Subject: [PATCH 34/39] Add version string to favicon to force refresh

---
 airtime_mvc/application/Bootstrap.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php
index 9f2df5175..0c1911d55 100644
--- a/airtime_mvc/application/Bootstrap.php
+++ b/airtime_mvc/application/Bootstrap.php
@@ -144,7 +144,9 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
 
         $baseUrl = Application_Common_OsPath::getBaseDir();
 
-        $view->headLink(array('rel' => 'icon', 'href' => $baseUrl . 'favicon.ico', 'type' => 'image/x-icon'), 'PREPEND')
+        $view->headLink(array('rel' => 'icon',
+                              'href' => $baseUrl . 'favicon.ico?' . $CC_CONFIG['airtime_version'],
+                              'type' => 'image/x-icon'), 'PREPEND')
             ->appendStylesheet($baseUrl . 'css/bootstrap.css?' . $CC_CONFIG['airtime_version'])
             ->appendStylesheet($baseUrl . 'css/redmond/jquery-ui-1.8.8.custom.css?' . $CC_CONFIG['airtime_version'])
             ->appendStylesheet($baseUrl . 'css/pro_dropdown_3.css?' . $CC_CONFIG['airtime_version'])

From 19fb67c8d67e2535fc128f14cc366f11fc4edeb4 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 29 Jun 2015 11:28:27 -0400
Subject: [PATCH 35/39] Fix broken delete button

---
 .../public/js/airtime/showbuilder/builder.js      | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js
index 60fdb3888..d46d0aade 100644
--- a/airtime_mvc/public/js/airtime/showbuilder/builder.js
+++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js
@@ -12,7 +12,8 @@ var AIRTIME = (function(AIRTIME){
         cursors = [],
         cursorIds = [],
         showInstanceIds = [],
-        headerFooter = [];
+        headerFooter = [],
+        DISABLED_CLASS = 'ui-state-disabled';
     
     if (AIRTIME.showbuilder === undefined) {
         AIRTIME.showbuilder = {};
@@ -135,8 +136,7 @@ var AIRTIME = (function(AIRTIME){
     mod.checkDeleteButton = function() {
         var $selected = $sbTable.find("tbody").find("input:checkbox").filter(":checked");
 
-        var button = $("#show_builder").find(".icon-trash").parent(),
-            DISABLED_CLASS = 'ui-state-disabled';
+        var button = $("#show_builder").find(".icon-trash").parent();
         if ($selected.length !== 0) {
             button.removeClass(DISABLED_CLASS);
             button.removeAttr('disabled');
@@ -535,9 +535,6 @@ var AIRTIME = (function(AIRTIME){
                     cl = 'sb-header';
                     
                     if (aData.record === true) {
-                        
-                        //headerIcon =  (aData.soundcloud_id > 0) ? "soundcloud" : "recording";
-                        
                         $div = $("<div/>", {
                             "class": "small-icon " + headerIcon
                         });
@@ -1191,8 +1188,10 @@ var AIRTIME = (function(AIRTIME){
         //delete selected tracks
         $toolbar.find('.icon-trash').parent()
             .click(function() {
-                
-                if (AIRTIME.button.isDisabled('icon-trash', true) === true) {
+
+                var button = $("#show_builder").find(".icon-trash").parent();
+
+                if (button.hasClass(DISABLED_CLASS)) {
                     return;
                 }
                 

From 133f1b72ca2f1d0ce969b031ef294eecaf460cf6 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 29 Jun 2015 11:32:28 -0400
Subject: [PATCH 36/39] Remove min-height on datatables for now

---
 airtime_mvc/public/css/styles.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css
index cf13b0f6d..f815ec819 100644
--- a/airtime_mvc/public/css/styles.css
+++ b/airtime_mvc/public/css/styles.css
@@ -1250,7 +1250,7 @@ input[type="checkbox"] {
 }
 
 .dataTables_scrolling {
-    min-height: 150px;
+    /*min-height: 150px;*/
 }
 
 .ui-dialog #schedule_playlist_dialog.ui-dialog-content {

From dd2ffd148d2d9c88f23a21ccd53e09f137b20ea8 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 29 Jun 2015 12:20:39 -0400
Subject: [PATCH 37/39] Fix bug where pypo fails to restart liquidsoap;
 uncomment adding pypo template files

---
 python_apps/pypo/pypo/pypofetch.py | 2 +-
 python_apps/pypo/setup.py          | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/python_apps/pypo/pypo/pypofetch.py b/python_apps/pypo/pypo/pypofetch.py
index 3772e170c..598d2abbc 100644
--- a/python_apps/pypo/pypo/pypofetch.py
+++ b/python_apps/pypo/pypo/pypofetch.py
@@ -185,7 +185,7 @@ class PypoFetch(Thread):
 
 
             self.logger.info("Restarting Liquidsoap")
-            subprocess.call('/etc/init.d/airtime-liquidsoap restart', shell=True, close_fds=True)
+            subprocess.call('kill -9 `pidof airtime-liquidsoap`', shell=True, close_fds=True)
 
             #Wait here and poll Liquidsoap until it has started up
             self.logger.info("Waiting for Liquidsoap to start")
diff --git a/python_apps/pypo/setup.py b/python_apps/pypo/setup.py
index 3f83fb109..76011e72d 100644
--- a/python_apps/pypo/setup.py
+++ b/python_apps/pypo/setup.py
@@ -18,8 +18,8 @@ else:
             pypo_files.append(os.path.join(root, filename))
         
     data_files = [
-                  # ('/etc/init', ['install/upstart/airtime-playout.conf.template']),
-                  # ('/etc/init', ['install/upstart/airtime-liquidsoap.conf.template']),
+                  ('/etc/init', ['install/upstart/airtime-playout.conf.template']),
+                  ('/etc/init', ['install/upstart/airtime-liquidsoap.conf.template']),
                   ('/etc/init.d', ['install/sysvinit/airtime-playout']),
                   ('/etc/init.d', ['install/sysvinit/airtime-liquidsoap']),
                   ('/var/log/airtime/pypo', []),

From 04290e28dcdf6953eec665301a415621bae6d399 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 29 Jun 2015 14:25:00 -0400
Subject: [PATCH 38/39] Fix error getting current schedule information if
 connected to master source

---
 airtime_mvc/application/models/Schedule.php | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php
index 8c4dbcb40..07e72136b 100644
--- a/airtime_mvc/application/models/Schedule.php
+++ b/airtime_mvc/application/models/Schedule.php
@@ -200,14 +200,14 @@ SQL;
         // track information to the current show values
         if ($source != self::SCHEDULED_SOURCE_NAME) {
             $show = Application_Model_Show::getCurrentShow();
-            $results["current"] = array(
-                "starts" => $show[0]["starts"],
-                "ends" => $show[0]["ends"],
-                "type" => _("livestream"),
-                "name" => (isset($show[0])?$show[0]["name"]:"") . " - " . _(self::LIVE_STREAM),
+            $results["current"] = isset($show[0]) ? array(
+                "starts"            => $show[0]["starts"],
+                "ends"              => $show[0]["ends"],
+                "type"              => _("livestream"),
+                "name"              => $show[0]["name"] . " - " . _(self::LIVE_STREAM),
                 "media_item_played" => false,
-                "record" => "0"
-            );
+                "record"            => "0"
+            ) : null;
         } else if (count($rows) >= 1) {
             $currentMedia = $rows[0];
 

From a8f214803be4c65a41ff5ca1b8d118ed622fbe37 Mon Sep 17 00:00:00 2001
From: Duncan Sommerville <duncan.sommerville@gmail.com>
Date: Mon, 29 Jun 2015 17:07:17 -0400
Subject: [PATCH 39/39] Fix javascript error on some browsers

---
 .../public/js/airtime/library/events/library_showbuilder.js     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js
index 9674176f4..d44cd1707 100644
--- a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js
+++ b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js
@@ -11,7 +11,7 @@ var AIRTIME = (function(AIRTIME) {
         var selected = mod.getChosenItemsLength(), $cursor = $('tr.sb-selected'), check = false,
             shows = $('tr.sb-header'), current = $('tr.sb-current-show'),
         // TODO: this is an ugly way of doing this... we should find a more robust way of checking which view we're in.
-            cursorText = (window.location.href.toLowerCase().includes("schedule")) ? $.i18n._('Add to show') : $.i18n._('Add to next show');
+            cursorText = (window.location.href.toLowerCase().indexOf("schedule") > -1) ? $.i18n._('Add to show') : $.i18n._('Add to next show');
 
         // make sure library items are selected and a cursor is selected.
         if (selected !== 0) {