diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index 18ad96c9e..391de011f 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -38,7 +38,9 @@ require_once "Auth.php"; require_once 'Preference.php'; require_once 'Locale.php'; /* Enums */ +require_once "Enum.php"; require_once "MediaType.php"; +require_once "HttpRequestType.php"; /* Interfaces */ require_once "OAuth2.php"; require_once "OAuth2Controller.php"; diff --git a/airtime_mvc/application/common/PodcastManager.php b/airtime_mvc/application/common/PodcastManager.php index 7336fd09d..20c6f698d 100644 --- a/airtime_mvc/application/common/PodcastManager.php +++ b/airtime_mvc/application/common/PodcastManager.php @@ -50,13 +50,12 @@ class PodcastManager { $podcastArray = Application_Service_PodcastService::getPodcastById($podcast->getDbPodcastId()); $episodeList = $podcastArray["episodes"]; $episodes = array(); - // A bit hacky... sort the episodes by publication date to get the most recent - usort($episodeList, array(static::class, "_sortByEpisodePubDate")); for ($i = 0; $i < sizeof($episodeList); $i++) { $episodeData = $episodeList[$i]; + $ingestTimestamp = $podcast->getDbAutoIngestTimestamp(); // If the publication date of this episode is before the ingest timestamp, we don't need to ingest it // Since we're sorting by publication date, we can break - if ($episodeData["pub_date"] < $podcast->getDbAutoIngestTimestamp()) break; + if ($episodeData["pub_date"] < $ingestTimestamp) continue; $episode = PodcastEpisodesQuery::create()->findOneByDbEpisodeGuid($episodeData["guid"]); // Make sure there's no existing episode placeholder or import, and that the data is non-empty if (empty($episode) && !empty($episodeData)) { diff --git a/airtime_mvc/application/common/enum/HttpRequestType.php b/airtime_mvc/application/common/enum/HttpRequestType.php new file mode 100644 index 000000000..59b1a25b3 --- /dev/null +++ b/airtime_mvc/application/common/enum/HttpRequestType.php @@ -0,0 +1,12 @@ +view->layout()->disableLayout(); @@ -17,7 +12,6 @@ class Rest_PodcastController extends Zend_Rest_Controller // Remove reliance on .phtml files to render requests $this->_helper->viewRenderer->setNoRender(true); $this->view->setScriptPath(APPLICATION_PATH . 'views/scripts/'); - $this->_service = new Application_Service_PodcastEpisodeService(); } public function indexAction() @@ -120,11 +114,6 @@ class Rest_PodcastController extends Zend_Rest_Controller try { $requestData = json_decode($this->getRequest()->getRawBody(), true); - // Create placeholders in PodcastEpisodes so we know these episodes are being downloaded - // to prevent the user from trying to download them again while Celery is running - $episodes = $this->_service->addPodcastEpisodePlaceholders($requestData["podcast"]["id"], - $requestData["podcast"]["episodes"]); - $this->_service->downloadEpisodes($episodes); $podcast = Application_Service_PodcastService::updatePodcastFromArray($id, $requestData); $this->getResponse() @@ -167,7 +156,7 @@ class Rest_PodcastController extends Zend_Rest_Controller * Endpoint for performing bulk actions (deleting multiple podcasts, opening multiple editors) */ public function bulkAction() { - if ($this->_request->getMethod() != "POST") { + if ($this->_request->getMethod() != HttpRequestType::POST) { $this->getResponse() ->setHttpResponseCode(405) ->appendBody("ERROR: Method not accepted"); @@ -175,28 +164,20 @@ class Rest_PodcastController extends Zend_Rest_Controller } $ids = $this->_getParam('ids', []); - $method = $this->_getParam('method', 'GET'); + $method = $this->_getParam('method', HttpRequestType::GET); $responseBody = []; switch($method) { - case "DELETE": + case HttpRequestType::DELETE: foreach($ids as $id) { Application_Service_PodcastService::deletePodcastById($id); } // XXX: do we need this to be more descriptive? $responseBody = "Successfully deleted podcasts"; break; - case "GET": + case HttpRequestType::GET: foreach($ids as $id) { - // Check the StationPodcast table rather than checking - // the station podcast ID key in preferences for extensibility - $podcast = StationPodcastQuery::create()->findOneByDbPodcastId($id); - $path = $podcast ? 'podcast/station_podcast.phtml' : 'podcast/podcast.phtml'; - $podcast = Application_Service_PodcastService::getPodcastById($id); - $responseBody[] = array( - "podcast"=>json_encode($podcast), - "html"=>$this->view->render($path), - ); + $responseBody[] = Application_Service_PodcastService::buildPodcastEditorResponse($id, $this->view); } break; } diff --git a/airtime_mvc/application/modules/rest/controllers/PodcastEpisodesController.php b/airtime_mvc/application/modules/rest/controllers/PodcastEpisodesController.php index c9b7e249c..691506f46 100644 --- a/airtime_mvc/application/modules/rest/controllers/PodcastEpisodesController.php +++ b/airtime_mvc/application/modules/rest/controllers/PodcastEpisodesController.php @@ -2,12 +2,19 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller { + + /** + * @var Application_Service_PodcastEpisodeService + */ + protected $_service; + public function init() { $this->view->layout()->disableLayout(); // Remove reliance on .phtml files to render requests $this->_helper->viewRenderer->setNoRender(true); + $this->_service = new Application_Service_PodcastEpisodeService(); } public function indexAction() @@ -64,7 +71,7 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller public function getAction() { - //TODO: can we delete this? + // podcast ID $id = $this->getId(); if (!$id) { return; @@ -78,7 +85,7 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller try { $this->getResponse() ->setHttpResponseCode(201) - ->appendBody(json_encode(Application_Service_PodcastEpisodeService::getPodcastEpisodeById($episodeId))); + ->appendBody(json_encode($this->_service->getPodcastEpisodeById($episodeId))); } catch (PodcastNotFoundException $e) { $this->podcastNotFoundResponse(); @@ -99,7 +106,8 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller if ($episodeId = $this->_getParam('episode_id', false)) { $resp = $this->getResponse(); $resp->setHttpResponseCode(400); - $resp->appendBody("ERROR: Episode ID should not be specified when using POST. POST is only used for importing podcast episodes, and an episode ID will be chosen by Airtime"); + $resp->appendBody("ERROR: Episode ID should not be specified when using POST. POST is only used for " + . "importing podcast episodes, and an episode ID will be chosen by Airtime"); return; } @@ -111,10 +119,7 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller try { $requestData = json_decode($this->getRequest()->getRawBody(), true); - //$requestData = $this->getRequest()->getPost(); - - $episode = Application_Service_PodcastEpisodeService::createPodcastEpisode($id, $requestData); - + $episode = $this->_service->importEpisode($id, $requestData["episode"]); $this->getResponse() ->setHttpResponseCode(201) ->appendBody(json_encode($episode)); @@ -128,9 +133,6 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller public function deleteAction() { - Logging::info("delete - episodes"); - - //TODO: can we delete this? $id = $this->getId(); if (!$id) { return; @@ -142,7 +144,7 @@ class Rest_PodcastEpisodesController extends Zend_Rest_Controller } try { - Application_Service_PodcastEpisodeService::deletePodcastEpisodeById($episodeId); + $this->_service->deletePodcastEpisodeById($episodeId); $this->getResponse() ->setHttpResponseCode(204); } catch (PodcastEpisodeNotFoundException $e) { diff --git a/airtime_mvc/application/services/PodcastEpisodeService.php b/airtime_mvc/application/services/PodcastEpisodeService.php index 6c2da74a2..527d41942 100644 --- a/airtime_mvc/application/services/PodcastEpisodeService.php +++ b/airtime_mvc/application/services/PodcastEpisodeService.php @@ -31,6 +31,20 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir "id" ); + /** + * Utility function to import and download a single episode + * + * @param int $podcastId ID of the podcast the episode should belong to + * @param array $episode array of episode data to store + * + * @return PodcastEpisodes the stored PodcastEpisodes object + */ + public function importEpisode($podcastId, $episode) { + $e = $this->addPlaceholder($podcastId, $episode); + $this->_download($e->getDbId(), $e->getDbDownloadUrl()); + return $e; + } + /** * Given an array of episodes, store them in the database as placeholder objects until * they can be processed by Celery @@ -188,7 +202,7 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir return $episode->toArray(BasePeer::TYPE_FIELDNAME); } - public static function getPodcastEpisodes($podcastId) + public function getPodcastEpisodes($podcastId) { $podcast = PodcastQuery::create()->findPk($podcastId); if (!$podcast) { @@ -204,25 +218,7 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir return $episodesArray; } - public static function createPodcastEpisode($podcastId, $data) - { - self::removePrivateFields($data); - - try { - $episode = new PodcastEpisodes(); - $episode->setDbPodcastId($podcastId); - $episode->fromArray($data, BasePeer::TYPE_FIELDNAME); - $episode->save(); - - return $episode->toArray(BasePeer::TYPE_FIELDNAME); - } catch (Exception $e) { - $episode->delete(); - throw $e; - } - - } - - public static function deletePodcastEpisodeById($episodeId) + public function deletePodcastEpisodeById($episodeId) { $episode = PodcastEpisodesQuery::create()->findByDbId($episodeId); @@ -233,7 +229,7 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir } } - private static function removePrivateFields(&$data) + private function removePrivateFields(&$data) { foreach (self::$privateFields as $key) { unset($data[$key]); diff --git a/airtime_mvc/application/services/PodcastService.php b/airtime_mvc/application/services/PodcastService.php index 2ba264ff8..57d5ae304 100644 --- a/airtime_mvc/application/services/PodcastService.php +++ b/airtime_mvc/application/services/PodcastService.php @@ -291,6 +291,28 @@ class Application_Service_PodcastService } } + /** + * Build a response with podcast data and embedded HTML to load on the frontend + * + * @param int $podcastId ID of the podcast to build a response for + * @param Zend_View_Interface $view Zend view object to render the response HTML + * + * @return array the response array containing the podcast data and editor HTML + * + * @throws PodcastNotFoundException + */ + public static function buildPodcastEditorResponse($podcastId, $view) { + // Check the StationPodcast table rather than checking + // the station podcast ID key in preferences for extensibility + $podcast = StationPodcastQuery::create()->findOneByDbPodcastId($podcastId); + $path = $podcast ? 'podcast/station_podcast.phtml' : 'podcast/podcast.phtml'; + $podcast = Application_Service_PodcastService::getPodcastById($podcastId); + return array( + "podcast" => json_encode($podcast), + "html" => $view->render($path), + ); + } + /** * Updates a Podcast object with the given metadata * diff --git a/airtime_mvc/application/views/scripts/podcast/podcast_url_dialog.phtml b/airtime_mvc/application/views/scripts/podcast/podcast_url_dialog.phtml index 4494c297e..ae58aacbe 100644 --- a/airtime_mvc/application/views/scripts/podcast/podcast_url_dialog.phtml +++ b/airtime_mvc/application/views/scripts/podcast/podcast_url_dialog.phtml @@ -2,11 +2,11 @@
csrf ?>
\ No newline at end of file diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 2b8a273e5..0c0d2f681 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -1276,6 +1276,7 @@ var AIRTIME = (function(AIRTIME) { var ajaxSourceURL = baseUrl+"rest/podcast"; var podcastToolbarButtons = AIRTIME.widgets.Table.getStandardToolbarButtons(); + podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].title = $.i18n._('Add'); //"New" Podcast is misleading podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].eventHandlers.click = function(e) { AIRTIME.podcast.createUrlDialog(); }; diff --git a/airtime_mvc/public/js/airtime/library/podcast.js b/airtime_mvc/public/js/airtime/library/podcast.js index 5dcee4af1..85501d425 100644 --- a/airtime_mvc/public/js/airtime/library/podcast.js +++ b/airtime_mvc/public/js/airtime/library/podcast.js @@ -21,9 +21,13 @@ var AIRTIME = (function (AIRTIME) { tab.setName($scope.podcast.title); $scope.savePodcast = function() { - var podcastData = $scope.podcast; // Copy the podcast in scope so we can modify it - podcastData.episodes = episodeTable.getSelectedRows(); - $http.put(endpoint + $scope.podcast.id, { csrf_token: jQuery("#csrf").val(), podcast: podcastData }) + var episodes = episodeTable.getSelectedRows(), + csrf = jQuery("#csrf").val(); + // TODO: Should we implement a batch endpoint for this instead? + jQuery.each(episodes, function() { + $http.post(endpoint + $scope.podcast.id + '/episodes', { csrf_token: csrf, episode: this }); + }); + $http.put(endpoint + $scope.podcast.id, { csrf_token: csrf, podcast: $scope.podcast }) .success(function() { episodeTable.reload($scope.podcast.id); AIRTIME.library.podcastDataTable.fnDraw(); @@ -53,7 +57,8 @@ var AIRTIME = (function (AIRTIME) { }); if (ids.length > 0) { - // Bulk methods should use post because we're sending data in the request body + // Bulk methods should use post because we're sending data in the request body. There is no standard + // RESTful way to implement bulk actions, so this is how we do it: $.post(endpoint + "bulk", {csrf_token: $("#csrf").val(), method: method, ids: ids}, callback); } } diff --git a/airtime_mvc/public/js/airtime/widgets/table.js b/airtime_mvc/public/js/airtime/widgets/table.js index 165b1b2b7..5fe15b5a2 100644 --- a/airtime_mvc/public/js/airtime/widgets/table.js +++ b/airtime_mvc/public/js/airtime/widgets/table.js @@ -57,6 +57,7 @@ var AIRTIME = (function(AIRTIME) { "sAjaxSource": baseUrl+"rest/media", //Override me "sAjaxDataProp": "aaData", "bScrollCollapse": false, + "deferLoading" : 1, //0 tells it there's zero elements loaded and disables the automatic AJAX. We don't want to load until after we bind all our event handlers, to prevent a race condition with the "init" event callback. "sPaginationType": "full_numbers", "bJQueryUI": true, "bAutoWidth": false, @@ -75,6 +76,7 @@ var AIRTIME = (function(AIRTIME) { "sDom": 'Rf<"dt-process-rel"r><"H"<"table_toolbar"C>><"dataTables_scrolling"t<"#library_empty"<"#library_empty_image"><"#library_empty_text">>><"F"lip>>', "fnServerData": self._fetchData, + "fnInitComplete" : function() { self._setupEventHandlers(bItemSelection) } //"fnDrawCallback" : self._tableDrawCallback }; @@ -85,8 +87,7 @@ var AIRTIME = (function(AIRTIME) { } self._datatable = self._$wrapperDOMNode.dataTable(options); - self._setupEventHandlers(bItemSelection); - + self._datatable.fnDraw(); //Load the AJAX data now that our event handlers have been bound. //return self._datatable; return self; @@ -163,9 +164,9 @@ var AIRTIME = (function(AIRTIME) { .css('padding-right', f.outerWidth()); }); - $(self._datatable).on('init', function(e) { - self._setupToolbarButtons(self._toolbarButtons); - }); + //Since this function is already called when the datatables initialization is complete, we know the DOM + //structure for the datatable exists and can just proceed to setup the toolbar DOM elements now. + self._setupToolbarButtons(self._toolbarButtons); };