SAAS-1071 - more work on backend podcast implementation

This commit is contained in:
Duncan Sommerville 2015-09-24 12:58:02 -04:00
parent dcb30b3aa7
commit ca51dcf3ae
7 changed files with 119 additions and 62 deletions

View File

@ -38,7 +38,6 @@ class Podcast extends BasePodcast
* @throws InvalidPodcastException * @throws InvalidPodcastException
* @throws PodcastLimitReachedException * @throws PodcastLimitReachedException
*/ */
public static function create($data) public static function create($data)
{ {
if (Application_Service_PodcastService::podcastLimitReached()) { if (Application_Service_PodcastService::podcastLimitReached()) {
@ -98,24 +97,11 @@ class Podcast extends BasePodcast
$podcast->setDbType(IMPORTED_PODCAST); $podcast->setDbType(IMPORTED_PODCAST);
$podcast->save(); $podcast->save();
$podcastArray = $podcast->toArray(BasePeer::TYPE_FIELDNAME); return self::_generatePodcastArray($podcast, $rss);
$podcastArray["episodes"] = array();
foreach ($rss->get_items() as $item) {
array_push($podcastArray["episodes"], array(
"title" => $item->get_title(),
"author" => $item->get_author()->get_name(),
"description" => $item->get_description(),
"pubDate" => $item->get_date("Y-m-d H:i:s"),
"link" => $item->get_enclosure()->get_link()
));
}
return $podcastArray;
} catch(Exception $e) { } catch(Exception $e) {
$podcast->delete(); $podcast->delete();
throw $e; throw $e;
} }
} }
/** /**
@ -125,6 +111,7 @@ class Podcast extends BasePodcast
* @param $podcastId * @param $podcastId
* *
* @throws PodcastNotFoundException * @throws PodcastNotFoundException
* @throws InvalidPodcastException
* @return array - Podcast Array with a full list of episodes * @return array - Podcast Array with a full list of episodes
*/ */
public static function getPodcastById($podcastId) public static function getPodcastById($podcastId)
@ -136,10 +123,22 @@ class Podcast extends BasePodcast
$rss = Application_Service_PodcastService::getPodcastFeed($podcast->getDbUrl()); $rss = Application_Service_PodcastService::getPodcastFeed($podcast->getDbUrl());
if (!$rss) { if (!$rss) {
throw new PodcastNotFoundException(); throw new InvalidPodcastException();
} }
// FIXME: Get rid of this duplication and move into a new function (serializer/deserializer) return self::_generatePodcastArray($podcast, $rss);
}
/**
* Given a podcast object and a SimplePie feed object,
* generate a data array to pass back to the front-end
*
* @param Podcast $podcast Podcast model object
* @param SimplePie $rss SimplePie feed object
*
* @return array
*/
private static function _generatePodcastArray($podcast, $rss) {
$podcastArray = $podcast->toArray(BasePeer::TYPE_FIELDNAME); $podcastArray = $podcast->toArray(BasePeer::TYPE_FIELDNAME);
$podcastArray["episodes"] = array(); $podcastArray["episodes"] = array();
@ -148,8 +147,9 @@ class Podcast extends BasePodcast
"title" => $item->get_title(), "title" => $item->get_title(),
"author" => $item->get_author()->get_name(), "author" => $item->get_author()->get_name(),
"description" => $item->get_description(), "description" => $item->get_description(),
"pubDate" => $item->get_date("Y-m-d H:i:s"), "pub_date" => $item->get_date("Y-m-d H:i:s"),
"link" => $item->get_enclosure()->get_link() "link" => $item->get_link(),
"enclosure" => $item->get_enclosure()
)); ));
} }

View File

@ -2,6 +2,12 @@
class Rest_PodcastController extends Zend_Rest_Controller class Rest_PodcastController extends Zend_Rest_Controller
{ {
/**
* @var Application_Service_PodcastService
*/
protected $_service;
public function init() public function init()
{ {
$this->view->layout()->disableLayout(); $this->view->layout()->disableLayout();
@ -9,6 +15,7 @@ class Rest_PodcastController extends Zend_Rest_Controller
// Remove reliance on .phtml files to render requests // Remove reliance on .phtml files to render requests
$this->_helper->viewRenderer->setNoRender(true); $this->_helper->viewRenderer->setNoRender(true);
$this->view->setScriptPath(APPLICATION_PATH . 'views/scripts/'); $this->view->setScriptPath(APPLICATION_PATH . 'views/scripts/');
$this->_service = new Application_Service_PodcastService();
} }
public function indexAction() public function indexAction()
@ -119,6 +126,7 @@ class Rest_PodcastController extends Zend_Rest_Controller
try { try {
$requestData = json_decode($this->getRequest()->getRawBody(), true); $requestData = json_decode($this->getRequest()->getRawBody(), true);
$this->_service->downloadEpisodes($requestData["podcast"]["episodes"]);
$podcast = Podcast::updateFromArray($id, $requestData); $podcast = Podcast::updateFromArray($id, $requestData);
$this->getResponse() $this->getResponse()

View File

@ -56,7 +56,7 @@ class Application_Service_PodcastService extends Application_Service_ThirdPartyC
$feed->enable_cache(false); $feed->enable_cache(false);
$feed->init(); $feed->init();
return $feed; return $feed;
} catch (FeedException $e) { } catch (Exception $e) {
return false; return false;
} }
} }
@ -66,15 +66,28 @@ class Application_Service_PodcastService extends Application_Service_ThirdPartyC
} }
/** /**
* Given an array of track identifiers, download RSS feed tracks * Given an array of episodes, extract the download URLs and send them to Celery
* *
* @param array $trackIds array of track identifiers to download * @param array $episodes array of podcast episodes
*/
public function downloadEpisodes($episodes) {
$episodeUrls = array();
foreach($episodes as $episode) {
array_push($episodeUrls, $episode["enclosure"]["link"]);
}
$this->_download($episodeUrls);
}
/**
* Given an array of download URLs, download RSS feed tracks
*
* @param array $downloadUrls array of download URLs to send to Celery
* TODO: do we need other parameters here...? * TODO: do we need other parameters here...?
*/ */
public function download($trackIds) { private function _download($downloadUrls) {
$CC_CONFIG = Config::getConfig(); $CC_CONFIG = Config::getConfig();
$data = array( $data = array(
// 'download_urls' => , TODO: get download urls to send to Celery 'download_urls' => $downloadUrls,
'callback_url' => Application_Common_HTTPHelper::getStationUrl() . '/rest/media', 'callback_url' => Application_Common_HTTPHelper::getStationUrl() . '/rest/media',
'api_key' => $apiKey = $CC_CONFIG["apiKey"][0], 'api_key' => $apiKey = $CC_CONFIG["apiKey"][0],
); );
@ -87,8 +100,8 @@ class Application_Service_PodcastService extends Application_Service_ThirdPartyC
* Update a ThirdPartyTrackReferences object for a completed upload * Update a ThirdPartyTrackReferences object for a completed upload
* *
* @param $task CeleryTasks the completed CeleryTasks object * @param $task CeleryTasks the completed CeleryTasks object
* @param $trackId int ThirdPartyTrackReferences identifier * @param $episodeId int PodcastEpisodes identifier
* @param $track object third-party service track object * @param $episode object object containing Podcast episode information
* @param $status string Celery task status * @param $status string Celery task status
* *
* @return ThirdPartyTrackReferences the updated ThirdPartyTrackReferences object * @return ThirdPartyTrackReferences the updated ThirdPartyTrackReferences object
@ -96,14 +109,14 @@ class Application_Service_PodcastService extends Application_Service_ThirdPartyC
* @throws Exception * @throws Exception
* @throws PropelException * @throws PropelException
*/ */
function updateTrackReference($task, $trackId, $track, $status) { public function updateTrackReference($task, $episodeId, $episode, $status) {
$ref = parent::updateTrackReference($task, $trackId, $track, $status); $ref = parent::updateTrackReference($task, $episodeId, $episode, $status);
if ($status == CELERY_SUCCESS_STATUS) { if ($status == CELERY_SUCCESS_STATUS) {
// TODO: handle successful download // TODO: handle successful download
// $ref->setDbForeignId(); // $ref->setDbForeignId();
// FIXME: we need the file ID here, but 'track' is too arbitrary... // FIXME: we need the file ID here, but 'track' is too arbitrary...
$ref->setDbFileId($track->fileId); $ref->setDbFileId($episode->fileId);
} }
$ref->save(); $ref->save();

View File

@ -6,14 +6,24 @@
<div class="inner_editor_wrapper"> <div class="inner_editor_wrapper">
<form class="podcast-metadata"> <form class="podcast-metadata">
<input ng-value="podcast.id" class="obj_id" type="hidden"/> <input ng-value="podcast.id" class="obj_id" type="hidden"/>
<label> <p>
<?php echo _("Podcast Name") ?> <label for="podcast_name">
<input disabled ng-model="podcast.title" type="text"/> <?php echo _("Podcast Name") ?>
</label> </label>
<label> <input disabled name="podcast_name" ng-model="podcast.title" type="text"/>
<?php echo _("Podcast URL") ?> </p>
<input disabled ng-model="podcast.url" type="text"/> <p>
</label> <label for="podcast_url">
<?php echo _("Podcast URL") ?>
</label>
<input disabled name="podcast_url" ng-model="podcast.url" type="text"/>
</p>
<p>
<label for="podcast_auto_ingest">
<?php echo _("Automatically download latest episodes?") ?>
</label>
<input name="podcast_auto_ingest" ng-model="podcast.auto_ingest" type="checkbox"/>
</p>
</form> </form>
</div> </div>

View File

@ -11,7 +11,8 @@ var AIRTIME = (function (AIRTIME) {
//AngularJS app //AngularJS app
var podcastApp = angular.module('podcast', []) var podcastApp = angular.module('podcast', [])
.controller('RestController', function($scope, $http, podcast, tab) { .controller('RestController', function($scope, $http, podcast, tab, episodeTable) {
// We need to pass in the tab object and the episodes table object so we can reference them
//We take a podcast object in as a parameter rather fetching the podcast by ID here because //We take a podcast object in as a parameter rather fetching the podcast by ID here because
//when you're creating a new podcast, we already have the object from the result of the POST. We're saving //when you're creating a new podcast, we already have the object from the result of the POST. We're saving
@ -20,7 +21,9 @@ var AIRTIME = (function (AIRTIME) {
tab.setName($scope.podcast.title); tab.setName($scope.podcast.title);
$scope.savePodcast = function() { $scope.savePodcast = function() {
$http.put(endpoint + $scope.podcast.id, { csrf_token: jQuery("#csrf").val(), podcast: $scope.podcast }) 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 })
.success(function() { .success(function() {
// TODO // TODO
}); });
@ -44,9 +47,10 @@ var AIRTIME = (function (AIRTIME) {
$.post(endpoint + "bulk", { csrf_token: $("#csrf").val(), method: method, ids: ids }, callback); $.post(endpoint + "bulk", { csrf_token: $("#csrf").val(), method: method, ids: ids }, callback);
} }
function _bootstrapAngularApp(podcast, tab) { function _bootstrapAngularApp(podcast, tab, table) {
podcastApp.value('podcast', podcast); podcastApp.value('podcast', podcast);
podcastApp.value('tab', tab); podcastApp.value('tab', tab);
podcastApp.value('episodeTable', table);
var wrapper = tab.contents.find(".editor_pane_wrapper"); var wrapper = tab.contents.find(".editor_pane_wrapper");
wrapper.attr("ng-controller", "RestController"); wrapper.attr("ng-controller", "RestController");
angular.bootstrap(wrapper.get(0), ["podcast"]); angular.bootstrap(wrapper.get(0), ["podcast"]);
@ -70,9 +74,9 @@ var AIRTIME = (function (AIRTIME) {
var podcast = JSON.parse(json.podcast); var podcast = JSON.parse(json.podcast);
var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+podcast.id, var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+podcast.id,
tab = AIRTIME.tabs.openTab(json, uid, null); tab = AIRTIME.tabs.openTab(json, uid, null);
_bootstrapAngularApp(podcast, tab); var table = mod.initPodcastEpisodeDatatable(podcast.episodes);
_bootstrapAngularApp(podcast, tab, table);
$("#podcast_url_dialog").dialog("close"); $("#podcast_url_dialog").dialog("close");
mod.initPodcastEpisodeDatatable(podcast.episodes);
}); });
}; };
@ -82,8 +86,8 @@ var AIRTIME = (function (AIRTIME) {
var podcast = JSON.parse(el.podcast); var podcast = JSON.parse(el.podcast);
var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+podcast.id, var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+podcast.id,
tab = AIRTIME.tabs.openTab(el, uid, null); tab = AIRTIME.tabs.openTab(el, uid, null);
_bootstrapAngularApp(podcast, tab); var table = mod.initPodcastEpisodeDatatable(podcast.episodes);
mod.initPodcastEpisodeDatatable(podcast.episodes); _bootstrapAngularApp(podcast, tab, table);
}); });
}); });
}; };
@ -102,13 +106,13 @@ var AIRTIME = (function (AIRTIME) {
/* Author */ { "sTitle" : $.i18n._("Author") , "mDataProp" : "author" , "sClass" : "podcast_episodes_author" , "sWidth" : "170px" }, /* Author */ { "sTitle" : $.i18n._("Author") , "mDataProp" : "author" , "sClass" : "podcast_episodes_author" , "sWidth" : "170px" },
/* Description */ { "sTitle" : $.i18n._("Description") , "mDataProp" : "description" , "sClass" : "podcast_episodes_description" , "sWidth" : "300px" }, /* Description */ { "sTitle" : $.i18n._("Description") , "mDataProp" : "description" , "sClass" : "podcast_episodes_description" , "sWidth" : "300px" },
/* Link */ { "sTitle" : $.i18n._("Link") , "mDataProp" : "link" , "sClass" : "podcast_episodes_link" , "sWidth" : "170px" }, /* Link */ { "sTitle" : $.i18n._("Link") , "mDataProp" : "link" , "sClass" : "podcast_episodes_link" , "sWidth" : "170px" },
/* Publication Date */ { "sTitle" : $.i18n._("Publication Date") , "mDataProp" : "pubDate" , "sClass" : "podcast_episodes_pub_date" , "sWidth" : "170px" } /* Publication Date */ { "sTitle" : $.i18n._("Publication Date") , "mDataProp" : "pub_date" , "sClass" : "podcast_episodes_pub_date" , "sWidth" : "170px" }
]; ];
var podcastToolbarButtons = AIRTIME.widgets.Table.getStandardToolbarButtons(); var podcastToolbarButtons = AIRTIME.widgets.Table.getStandardToolbarButtons();
// Set up the div with id "podcast_table" as a datatable. // Set up the div with id "podcast_table" as a datatable.
mod.podcastEpisodesTableWidget = new AIRTIME.widgets.Table( var podcastEpisodesTableWidget = new AIRTIME.widgets.Table(
AIRTIME.tabs.getActiveTab().contents.find('#podcast_episodes'), // DOM node to create the table inside. AIRTIME.tabs.getActiveTab().contents.find('#podcast_episodes'), // DOM node to create the table inside.
true, // Enable item selection true, // Enable item selection
podcastToolbarButtons, // Toolbar buttons podcastToolbarButtons, // Toolbar buttons
@ -116,11 +120,17 @@ var AIRTIME = (function (AIRTIME) {
'aoColumns' : aoColumns, 'aoColumns' : aoColumns,
'bServerSide': false, 'bServerSide': false,
'sAjaxSource' : null, 'sAjaxSource' : null,
'aaData' : episodes 'aaData' : episodes,
"oColVis": {
"sAlign": "right",
"aiExclude": [0, 1],
"buttonText": $.i18n._("Columns"),
"iOverlayFade": 0
}
}); });
mod.podcastEpisodesDatatable = mod.podcastEpisodesTableWidget.getDatatable(); podcastEpisodesTableWidget.getDatatable().textScroll("td");
mod.podcastEpisodesDatatable.textScroll("td"); return podcastEpisodesTableWidget;
}; };
return AIRTIME; return AIRTIME;

22
composer.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "8d8a51740ad37127ff6618f80861ccfc", "hash": "8d8a51740ad37127ff6618f80861ccfc",
@ -231,7 +231,7 @@
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"predis/predis": "0.8.5", "predis/predis": ">=0.8.5",
"videlalvaro/php-amqplib": ">=2.4.0" "videlalvaro/php-amqplib": ">=2.4.0"
}, },
"type": "library", "type": "library",
@ -363,29 +363,32 @@
}, },
{ {
"name": "predis/predis", "name": "predis/predis",
"version": "v0.8.5", "version": "v1.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nrk/predis.git", "url": "https://github.com/nrk/predis.git",
"reference": "5f2eea628eb465d866ad2771927d83769c8f956c" "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nrk/predis/zipball/5f2eea628eb465d866ad2771927d83769c8f956c", "url": "https://api.github.com/repos/nrk/predis/zipball/84060b9034d756b4d79641667d7f9efe1aeb8e04",
"reference": "5f2eea628eb465d866ad2771927d83769c8f956c", "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.3.2" "php": ">=5.3.2"
}, },
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"suggest": { "suggest": {
"ext-curl": "Allows access to Webdis when paired with phpiredis", "ext-curl": "Allows access to Webdis when paired with phpiredis",
"ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"psr-0": { "psr-4": {
"Predis": "lib/" "Predis\\": "src/"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@ -406,7 +409,7 @@
"predis", "predis",
"redis" "redis"
], ],
"time": "2014-01-16 14:10:29" "time": "2015-07-30 18:34:15"
}, },
{ {
"name": "propel/propel1", "name": "propel/propel1",
@ -700,6 +703,7 @@
"simplepie/simplepie": 20 "simplepie/simplepie": 20
}, },
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false,
"platform": [], "platform": [],
"platform-dev": [] "platform-dev": []
} }

View File

@ -3,8 +3,12 @@ import json
import urllib2 import urllib2
import requests import requests
import soundcloud import soundcloud
import cgi
import urlparse
import posixpath
from celery import Celery from celery import Celery
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from contextlib import closing
celery = Celery() celery = Celery()
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
@ -93,10 +97,18 @@ def podcast_download(download_urls, callback_url, api_key):
""" """
try: try:
for url in download_urls: for url in download_urls:
r = requests.get(url, stream=True) with closing(requests.get(url, stream=True)) as r:
r.raise_for_status() # Try to get the filename from the content disposition
with r as f: d = r.headers.get('Content-Disposition')
requests.post(callback_url, data=f, auth=requests.auth.HTTPBasicAuth(api_key, '')) if d:
_, params = cgi.parse_header(d)
filename = params['filename']
else:
# Since we don't necessarily get the filename back in the response headers,
# parse the URL and get the filename and extension
path = urlparse.urlsplit(r.url).path
filename = posixpath.basename(path)
requests.post(callback_url, files={'file': (filename, r.content)}, auth=requests.auth.HTTPBasicAuth(api_key, ''))
except Exception as e: except Exception as e:
logger.info('Error during file download: {0}'.format(e.message)) logger.info('Error during file download: {0}'.format(e.message))
logger.info(str(e)) logger.info(str(e))