diff --git a/airtime_mvc/application/common/FileDataHelper.php b/airtime_mvc/application/common/FileDataHelper.php
index 568b25d4d..6ad391dbc 100644
--- a/airtime_mvc/application/common/FileDataHelper.php
+++ b/airtime_mvc/application/common/FileDataHelper.php
@@ -14,6 +14,7 @@ class FileDataHelper {
"audio/aac" => "aac",
"audio/aacp" => "aac",
"audio/mp4" => "m4a",
+ "video/mp4" => "mp4",
"audio/x-flac" => "flac",
"audio/wav" => "wav",
"audio/x-wav" => "wav",
diff --git a/airtime_mvc/application/forms/SoundCloudPreferences.php b/airtime_mvc/application/forms/SoundCloudPreferences.php
index 987244ced..d6478fbf0 100644
--- a/airtime_mvc/application/forms/SoundCloudPreferences.php
+++ b/airtime_mvc/application/forms/SoundCloudPreferences.php
@@ -40,6 +40,7 @@ class Application_Form_SoundcloudPreferences extends Zend_Form_SubForm
$this->addElement('image', 'SoundCloudConnect', array(
'src' => 'http://connect.soundcloud.com/2/btn-connect-sc-l.png',
+ 'target' => '_blank',
'decorators' => array(
'ViewHelper'
)
@@ -47,6 +48,7 @@ class Application_Form_SoundcloudPreferences extends Zend_Form_SubForm
$this->addElement('image', 'SoundCloudDisconnect', array(
'src' => 'http://connect.soundcloud.com/2/btn-disconnect-l.png',
+ 'target' => '_blank',
'decorators' => array(
'ViewHelper'
)
diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index 810f56462..d3505d349 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -80,6 +80,12 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
+
diff --git a/airtime_mvc/application/services/PodcastEpisodeService.php b/airtime_mvc/application/services/PodcastEpisodeService.php
index 896691a3c..c9a11e9b2 100644
--- a/airtime_mvc/application/services/PodcastEpisodeService.php
+++ b/airtime_mvc/application/services/PodcastEpisodeService.php
@@ -267,6 +267,13 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
: $this->_getImportedPodcastEpisodeArray($podcast, $episodes);
}
+ /**
+ * Given an array of PodcastEpisodes objects from the Station Podcast,
+ * convert the episode data into array form
+ *
+ * @param array $episodes array of PodcastEpisodes to convert
+ * @return array
+ */
private function _getStationPodcastEpisodeArray($episodes) {
$episodesArray = array();
foreach ($episodes as $episode) {
@@ -277,28 +284,42 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
return $episodesArray;
}
+ /**
+ * Given an ImportedPodcast object and an array of stored PodcastEpisodes objects,
+ * fetch all episodes from the podcast RSS feed, and serialize them in a readable form
+ *
+ * TODO: there's definitely a better approach than this... we should be trying to create
+ * PodcastEpisdoes objects instead of our own arrays
+ *
+ * @param ImportedPodcast $podcast Podcast object to fetch the episodes for
+ * @param array $episodes array of PodcastEpisodes objects to
+ *
+ * @return array array of episode data
+ *
+ * @throws CcFiles/FileNotFoundException
+ */
public function _getImportedPodcastEpisodeArray($podcast, $episodes) {
$rss = Application_Service_PodcastService::getPodcastFeed($podcast->getDbUrl());
$episodeIds = array();
$episodeFiles = array();
foreach ($episodes as $e) {
+ /** @var PodcastEpisodes $e */
array_push($episodeIds, $e->getDbEpisodeGuid());
$episodeFiles[$e->getDbEpisodeGuid()] = $e->getDbFileId();
}
$episodesArray = array();
foreach ($rss->get_items() as $item) {
- // If the enclosure is empty, this isn't a podcast episode
+ /** @var SimplePie_Item $item */
+ // If the enclosure is empty or has not URL, this isn't a podcast episode (there's no audio data)
$enclosure = $item->get_enclosure();
$url = $enclosure instanceof SimplePie_Enclosure ? $enclosure->get_link() : $enclosure["link"];
- if (empty($url)) {
- continue;
- }
+ if (empty($url)) { continue; }
$itemId = $item->get_id();
$ingested = in_array($itemId, $episodeIds) ? (empty($episodeFiles[$itemId]) ? -1 : 1) : 0;
$file = $ingested > 0 && !empty($episodeFiles[$itemId]) ?
CcFiles::getSanitizedFileById($episodeFiles[$itemId]) : array();
- /** @var SimplePie_Item $item */
+
array_push($episodesArray, array(
"podcast_id" => $podcast->getDbId(),
"guid" => $itemId,
@@ -306,7 +327,7 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
"title" => $item->get_title(),
// From the RSS spec best practices:
// 'An item's author element provides the e-mail address of the person who wrote the item'
- "author" => $item->get_author()->get_email(),
+ "author" => $this->_buildAuthorString($item),
"description" => htmlspecialchars($item->get_description()),
"pub_date" => $item->get_gmdate(),
"link" => $item->get_link(),
@@ -318,6 +339,23 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
return $episodesArray;
}
+ /**
+ * Construct a string representation of the author fields of a SimplePie_Item object
+ *
+ * @param SimplePie_Item $item the SimplePie_Item to extract the author data from
+ *
+ * @return string the string representation of the author data
+ */
+ private function _buildAuthorString(SimplePie_Item $item) {
+ $authorString = $author = $item->get_author();
+ if (!empty($author)) {
+ $authorString = $author->get_email();
+ $authorString = empty($authorString) ? $author->get_name() : $authorString;
+ }
+
+ return $authorString;
+ }
+
public function deletePodcastEpisodeById($episodeId)
{
$episode = PodcastEpisodesQuery::create()->findByDbId($episodeId);
diff --git a/airtime_mvc/application/views/scripts/partialviews/dashboard-sub-nav.php b/airtime_mvc/application/views/scripts/partialviews/dashboard-sub-nav.php
index fe9428f1e..6dd9214c6 100644
--- a/airtime_mvc/application/views/scripts/partialviews/dashboard-sub-nav.php
+++ b/airtime_mvc/application/views/scripts/partialviews/dashboard-sub-nav.php
@@ -21,10 +21,4 @@
-
-
+
\ 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 3c7cf7151..382a07c81 100644
--- a/airtime_mvc/public/js/airtime/library/library.js
+++ b/airtime_mvc/public/js/airtime/library/library.js
@@ -1255,7 +1255,9 @@ var AIRTIME = (function(AIRTIME) {
* @param table the table to show
*/
mod.setCurrentTable = function (table) {
- if (oTable) oTable.fnClearTable();
+ if (oTable && oTable === $datatables[mod.DataTableTypeEnum.PODCAST_EPISODES]) {
+ oTable.fnClearTable();
+ }
var dt = $datatables[table],
wrapper = $(dt).closest(".dataTables_wrapper");
$("#library_content").find(".dataTables_wrapper").hide();
@@ -1718,8 +1720,9 @@ var validationTypes = {
$(document).ready(function() {
-
- AIRTIME.library.initPodcastDatatable();
+ if (window.location.href.indexOf("showbuilder") > -1) {
+ AIRTIME.library.initPodcastDatatable();
+ }
$("#advanced-options").on("click", function() {
resizeAdvancedSearch();
diff --git a/airtime_mvc/public/js/airtime/library/podcast.js b/airtime_mvc/public/js/airtime/library/podcast.js
index cecdb7e91..0f7edf00e 100644
--- a/airtime_mvc/public/js/airtime/library/podcast.js
+++ b/airtime_mvc/public/js/airtime/library/podcast.js
@@ -146,10 +146,21 @@ var AIRTIME = (function (AIRTIME) {
* Sets up the internal datatable.
*/
PodcastController.prototype.initialize = function() {
+ var self = this;
// TODO: this solves a race condition, but we should look for the root cause
AIRTIME.tabs.onResize();
- this.$scope.tab.setName(this.$scope.podcast.title);
- this._initTable();
+ self.$scope.tab.setName(self.$scope.podcast.title);
+ self._initTable();
+ // Add an onclose hook to the tab to remove the table object and the
+ // import listener so we don't cause memory leaks.
+ if (self.episodeTable) {
+ self.$scope.tab.assignOnCloseHandler(function () {
+ self.episodeTable.destroy();
+ self.episodeTable = null;
+ self.$scope.tab = null;
+ self.$scope.$destroy();
+ });
+ }
};
/**
@@ -444,6 +455,7 @@ var AIRTIME = (function (AIRTIME) {
self.importListener = setInterval(function () {
var podcastId = self.config.podcastId, pendingRows = [];
if (!podcastId) return false;
+ console.log(self);
var dt = self.getDatatable(), data = dt.fnGetData();
// Iterate over the table data to check for any rows pending import
$.each(data, function () {
@@ -475,6 +487,13 @@ var AIRTIME = (function (AIRTIME) {
}
}, 10000); // Run every 10 seconds
};
+
+ /**
+ * Explicit destructor
+ */
+ PodcastEpisodeTable.prototype.destroy = function () {
+ clearInterval(this.importListener);
+ }
}
/**
@@ -552,9 +571,9 @@ var AIRTIME = (function (AIRTIME) {
*/
mod.editSelectedEpisodes = function (episodes) {
$.each(episodes, function () {
- if (!Object.keys(this.file).length > 0) return false;
- var uid = AIRTIME.library.MediaTypeStringEnum.FILE + "_" + this.file.id;
- $.get(baseUrl + "library/edit-file-md/id/" + this.file.id, {format: "json"}, function (json) {
+ if (this.file && !Object.keys(this.file).length > 0) return false;
+ var fileId = this.file_id || this.file.id, uid = AIRTIME.library.MediaTypeStringEnum.FILE + "_" + fileId;
+ $.get(baseUrl + "library/edit-file-md/id/" + fileId, {format: "json"}, function (json) {
AIRTIME.playlist.fileMdEdit(json, uid);
});
});
@@ -568,7 +587,7 @@ var AIRTIME = (function (AIRTIME) {
*/
mod.importSelectedEpisodes = function (episodes, dt) {
$.each(episodes, function () {
- if (Object.keys(this.file).length > 0) return false;
+ if (this.file && Object.keys(this.file).length > 0) return false;
var podcastId = this.podcast_id;
$.post(endpoint + podcastId + '/episodes', JSON.stringify({
csrf_token: $("#csrf").val(),
diff --git a/airtime_mvc/public/js/airtime/library/publish.js b/airtime_mvc/public/js/airtime/library/publish.js
index a1ee4de7b..5eb362bc7 100644
--- a/airtime_mvc/public/js/airtime/library/publish.js
+++ b/airtime_mvc/public/js/airtime/library/publish.js
@@ -59,7 +59,7 @@ var AIRTIME = (function (AIRTIME) {
}
});
- if (Object.keys(data).length > 0) {
+ if (data && Object.keys(data).length > 0) {
$http.put(endpoint + mediaId + '/publish', {csrf_token: jQuery("#csrf").val(), sources: data})
.success(function () {
init();
diff --git a/airtime_mvc/public/js/airtime/schedule/schedule.js b/airtime_mvc/public/js/airtime/schedule/schedule.js
index 110054eeb..cb05b889e 100644
--- a/airtime_mvc/public/js/airtime/schedule/schedule.js
+++ b/airtime_mvc/public/js/airtime/schedule/schedule.js
@@ -216,7 +216,7 @@ function buildScheduleDialog (json, instance_id) {
buttons: [
{
text: $.i18n._("Ok"),
- "class": "btn",
+ class: "btn",
click: function() {
$(this).dialog("close");
//getUsabilityHint();
diff --git a/airtime_mvc/public/js/airtime/showbuilder/tabs.js b/airtime_mvc/public/js/airtime/showbuilder/tabs.js
index 0268c390e..93ad41911 100644
--- a/airtime_mvc/public/js/airtime/showbuilder/tabs.js
+++ b/airtime_mvc/public/js/airtime/showbuilder/tabs.js
@@ -136,6 +136,13 @@ var AIRTIME = (function(AIRTIME){
});
};
+ /**
+ * Internal destructor. Can be assigned via assignOnCloseHandler
+ *
+ * @private
+ */
+ Tab.prototype._destroy = function () {}
+
/**
* Assign the given function f as the click handler for the tab
*
@@ -164,6 +171,15 @@ var AIRTIME = (function(AIRTIME){
this.tab.find(".lib_pl_close").unbind("click").click(f);
};
+ /**
+ * Assign an implicit destructor
+ *
+ * @param {function} fn function to run when this Tab is destroyed
+ */
+ Tab.prototype.assignOnCloseHandler = function (fn) {
+ this._destroy = fn;
+ };
+
/**
* Open this tab in the right-hand pane and set it as the currently active tab
*/
@@ -205,6 +221,8 @@ var AIRTIME = (function(AIRTIME){
} else {
mod.onResize();
}
+
+ self._destroy();
};
/**