CC-5450 : Refactor Media Management

refactored webstreams, new/save is working
adding validators/filters for the url
separating out parsing of different webstream formats.
This commit is contained in:
Naomi 2013-11-01 18:02:10 -04:00
parent 01d48e7e79
commit 19a7e7eb5d
13 changed files with 569 additions and 22 deletions

View File

@ -186,5 +186,15 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
'action' => 'password-change',
)));
}
protected function _initAutoload () {
// configure new autoloader
$autoloader = new Zend_Application_Module_Autoloader (array ('namespace' => '', 'basePath' => APPLICATION_PATH));
// autoload form validators & filters definition
$autoloader->addResourceType ('Filter', 'forms/filters', 'Filter_');
$autoloader->addResourceType ('Validator', 'forms/validators', 'Validate_');
}
}

View File

@ -17,7 +17,22 @@ class WebstreamController extends Zend_Controller_Action
public function newAction()
{
//clear the session in case an old playlist was open: CC-4196
Application_Model_Library::changePlaylist(null, null);
$service = new Application_Service_WebstreamService();
$form = $service->makeWebstreamForm(null);
$form->setDefaults(array(
'name' => 'Unititled Webstream',
'hours' => 0,
'mins' => 30,
));
$this->view->action = "new";
$this->view->html = $form->render();
/*
$userInfo = Zend_Auth::getInstance()->getStorage()->read();
if (!$this->isAuthorized(-1)) {
// TODO: this header call does not actually print any error message
@ -44,6 +59,7 @@ class WebstreamController extends Zend_Controller_Action
$this->view->obj = new Application_Model_Webstream($webstream);
$this->view->action = "new";
$this->view->html = $this->view->render('webstream/webstream.phtml');
*/
}
public function editAction()
@ -117,20 +133,40 @@ class WebstreamController extends Zend_Controller_Action
public function saveAction()
{
$request = $this->getRequest();
$id = $request->getParam("id");
$parameters = array();
foreach (array('id','length','name','description','url') as $p) {
foreach (array('id','hours', 'mins', 'name','description','url') as $p) {
$parameters[$p] = trim($request->getParam($p));
}
Logging::info($parameters);
$service = new Application_Service_WebstreamService();
$form = $service->makeWebstreamForm(null);
if ($form->isValid($parameters)) {
Logging::info("form is valid");
$values = $form->getValues();
Logging::info($values);
$service->saveWebstream($values);
}
else {
Logging::info("form is not valid");
}
/*
if (!$this->isAuthorized($id)) {
header("Status: 401 Not Authorized");
return;
}
*/
/*
list($analysis, $mime, $mediaUrl, $di) = Application_Model_Webstream::analyzeFormData($parameters);
try {
if (Application_Model_Webstream::isValid($analysis)) {
@ -150,5 +186,6 @@ class WebstreamController extends Zend_Controller_Action
$this->view->streamId = -1;
$this->view->analysis = $analysis;
}
*/
}
}

View File

@ -0,0 +1,62 @@
<?php
class Application_Form_Webstream extends Zend_Form
{
public function init() {
$this->setDecorators(
array(
array('ViewScript', array('viewScript' => 'form/webstream.phtml'))
)
);
$id = new Zend_Form_Element_Hidden('id');
$id->setValidators(array(
new Zend_Validate_Int()
));
$this->addElement($id);
$name = new Zend_Form_Element_Text('name');
$name->setLabel(_('Firstname:'));
$name->setAttrib('class', 'input_text');
$name->addFilter('StringTrim');
$this->addElement($name);
$description = new Zend_Form_Element_Text('description');
$description->setLabel(_('Description:'));
$description->setAttrib('class', 'input_text_area');
$description->addFilter('StringTrim');
$description->setValidators(array(
new Zend_Validate_StringLength(array('max' => 512)),
));
$this->addElement($description);
$url = new Zend_Form_Element_Text('url');
$url->setLabel(_('Stream URL:'));
$url->setAttrib('class', 'input_text');
$url->addFilter('StringTrim');
$url->addFilter(new Filter_WebstreamRedirect());
$url->setValidators(array(
new Validate_WebstreamUrl(),
));
$this->addElement($url);
$hours = new Zend_Form_Element_Text('hours');
$hours->setLabel(_('h'));
$hours->setAttrib('class', 'input_text');
$hours->addFilter('StringTrim');
$hours->setValidators(array(
new Zend_Validate_Int(),
));
$this->addElement($hours);
$min = new Zend_Form_Element_Text('mins');
$min->setLabel(_('m'));
$min->setAttrib('class', 'input_text');
$min->addFilter('StringTrim');
$min->setValidators(array(
new Zend_Validate_Int(),
));
$this->addElement($min);
}
}

View File

@ -0,0 +1,51 @@
<?php
class Filter_WebstreamRedirect implements Zend_Filter_Interface
{
public function filter($value)
{
Logging::info("checking if $value passes filter");
//empty when first creating the form.
if (empty($value)) {
return $value;
}
// By default get_headers uses a GET request to fetch the headers. If you
// want to send a HEAD request instead, you can do so using a stream context:
//using max redirects to avoid mixed headers,
//can manually follow redirects if a Location header exists.
stream_context_set_default(
array(
'http' => array(
'method' => 'HEAD',
'max_redirects' => '1'
)
)
);
try {
$url = $value;
while (true) {
//get an associative array of headers.
$headers = get_headers($url, 1);
Logging::info($headers);
if (empty($headers["Location"])) {
//is not a redirect;
break;
}
else {
$url = $headers["Location"];
}
}
return $url;
}
catch (Exception $e) {
throw new Zend_Filter_Exception( _("Invalid webstream url"));
}
}
}

View File

@ -0,0 +1,87 @@
<?php
class Validate_WebstreamUrl extends Zend_Validate_Abstract
{
const INVALID_WEBSTREAM_URL = 'webstream_url';
const INVALID_MIME_TYPE = 'webstream_mime';
protected $_messageTemplates;
protected $_validMimeTypePatterns = array(
"/x-mpegurl/" => false,
"/xspf\+xml/" => false,
"/pls\+xml|x-scpls/" => false,
"/(mpeg|ogg|audio\/aacp)/" => true
);
public function __construct()
{
$this->_messageTemplates = array(
self::INVALID_WEBSTREAM_URL => _("Invalid webstream url"),
self::INVALID_MIME_TYPE => _("Invalid webstream mime type"),
);
}
private function validateNoContentLength($headers)
{
return isset($headers["Content-Length"]) ? false : true;
}
public function isValid($value)
{
Logging::info("checking if $value is valid");
// By default get_headers uses a GET request to fetch the headers. If you
// want to send a HEAD request instead, you can do so using a stream context:
//using max redirects to avoid mixed headers,
//can manually follow redirects if a Location header exists.
stream_context_set_default(
array(
'http' => array(
'method' => 'HEAD'
)
)
);
try {
//get an associative array of headers.
$headers = get_headers($value, 1);
if (empty($headers["Content-Type"])) {
$this->_error(self::INVALID_MIME_TYPE);
return false;
}
$contentType = $headers["Content-Type"];
Logging::info($contentType);
$isValid = false;
foreach ($this->_validMimeTypePatterns as $pattern => $doContentLengthCheck) {
if (preg_match($pattern, $contentType)) {
if ($doContentLengthCheck) {
$isValid = self::validateNoContentLength($headers);
}
else {
$isValid = true;
}
break;
}
}
if ($isValid === false) {
$this->_error(self::INVALID_MIME_TYPE);
}
return $isValid;
}
catch (Exception $e) {
//url is not real
$this->_error(self::INVALID_WEBSTREAM_URL);
return false;
}
}
}

View File

@ -18,4 +18,40 @@ use Airtime\MediaItem\om\BaseWebstream;
*/
class Webstream extends BaseWebstream
{
public function getHoursMins() {
return explode(":", $this->getLength());
}
public function getUrlData() {
$url = $this->getUrl();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// grab URL and pass it to the browser
//TODO: What if invalid url?
$content = curl_exec($ch);
// close cURL resource, and free up system resources
curl_close($ch);
return $content;
}
public function setUrl($v) {
parent::setUrl($v);
//get an associative array of headers.
$headers = get_headers($v, 1);
$mime = $headers["Content-Type"];
$this->setMime($mime);
return $this;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Airtime\MediaItem\Webstream;
use \Exception;
use \Logging;
class M3UWebstreamParser implements WebstreamParser {
public function getStreamUrl($webstream) {
try {
$content = $webstream->getUrlData();
//split into lines:
$delim = "\n";
if (strpos($content, "\r\n") !== false) {
$delim = "\r\n";
}
$lines = explode($delim, $content);
//TODO test the stream
return $lines[0];
}
catch (Exception $e) {
Logging::warn($e->getMessage());
throw new Exception(_("Could not parse M3U playlist"));
}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Airtime\MediaItem\Webstream;
use \Exception;
use \Logging;
class PLSWebstreamParser implements WebstreamParser {
public function getStreamUrl($webstream) {
try {
$content = $webstream->getUrlData();
$ini = parse_ini_string($content, true);
//TODO test the stream
return $ini["playlist"]["File1"];
}
catch (Exception $e) {
Logging::warn($e->getMessage());
throw new Exception(_("Could not parse PLS playlist"));
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Airtime\MediaItem\Webstream;
interface WebstreamParser {
//parses a m3u, pls, xspf etc format to get a valid stream url
public function getStreamUrl($webstream);
}

View File

@ -0,0 +1,33 @@
<?php
namespace Airtime\MediaItem\Webstream;
use \Exception;
use \Logging;
class XSPFWebstreamParser implements WebstreamParser {
public function getStreamUrl($webstream) {
try {
$content = $webstream->getUrlData();
$dom = new DOMDocument;
$dom->loadXML($content);
$tracks = $dom->getElementsByTagName('track');
foreach ($tracks as $track) {
$locations = $track->getElementsByTagName('location');
foreach ($locations as $loc) {
return $loc->nodeValue;
}
}
}
catch (Exception $e) {
Logging::warn($e->getMessage());
throw new Exception(_("Could not parse XSPF playlist"));
}
}
}

View File

@ -0,0 +1,87 @@
<?php
use Airtime\MediaItem\Webstream;
use Airtime\MediaItem\WebstreamQuery;
class Application_Service_WebstreamService
{
public function makeWebstreamForm($id, $populate = false) {
try {
$form = new Application_Form_Webstream();
if ($populate) {
$webstream = WebstreamQuery::create()->findPk($id);
$length = $webstream->getHoursMins();
$formValues = array(
"name" => $webstream->getName(),
"description" => $webstream->getDescription(),
"url" => $webstream->getUrl(),
"hours" => $length[0],
"mins" => $length[1]
);
$form->populate($formValues);
}
return $form;
}
catch (Exception $e) {
Logging::info($e);
throw $e;
}
}
private function buildFromFormValues($ws, $values) {
$hours = intval($values["hours"]);
$minutes = intval($values["mins"]);
if ($minutes > 59) {
//minutes cannot be over 59. Need to convert anything > 59 minutes into hours.
$hours += intval($minutes/60);
$minutes = $minutes%60;
}
$length = "$hours:$minutes";
$ws->setName($values["name"]);
$ws->setDescription($values["description"]);
$ws->setUrl($values["url"]);
$ws->setLength($length);
return $ws;
}
public function createWebstream($values) {
$ws = new Webstream();
$ws = self::buildFromFormValues($ws, $values);
return $ws;
}
public function updateWebstream($values) {
$ws = WebstreamQuery::create()->findPk($values["id"]);
$ws = self::buildFromFormValues($ws, $values);
return $ws;
}
public function saveWebstream($values) {
if ($values["id"] != null) {
$ws = self::updateWebstream($values);
}
else {
$ws = self::createWebstream($values);
}
$ws->save();
return $ws;
}
}

View File

@ -0,0 +1,69 @@
<?php $form = $this->element ?>
<a href="#" class="close-round" id="lib_pl_close"></a>
<div class="btn-toolbar spl-no-top-margin clearfix">
<div class="btn-group pull-left">
<button id="ws_new" class="btn dropdown-toggle" data-toggle="dropdown" aria-disabled="false">
<?php echo _("New")?> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li id='lib-new-pl'><a href="#"><?php echo _("New Playlist") ?></a></li>
<li id='lib-new-bl'><a href="#"><?php echo _("New Smart Block") ?></a></li>
<li id='lib-new-ws'><a href="#"><?php echo _("New Webstream") ?></a></li>
</ul>
</div>
<?php if (isset($form)) : ?>
<div class="btn-group pull-right">
<button class="btn btn-inverse" type="submit" id="webstream_save" name="submit"><?php echo _("Save") ?></button>
</div>
<div class="btn-group pull-right">
<button id="ws_delete" class="btn" <?php if ($this->action == "new"): ?>style="display:none;"<?php endif; ?>aria-disabled="false"><?php echo _("Delete") ?></button>
</div>
<?php endif; ?>
</div>
<?php if (isset($form)) : ?>
<input id="ws_id" type="hidden" value="<?php echo $form->getElement("id")->getValue() ?>"></input>
<input id="obj_lastMod" type="hidden" value="<?php echo "1";//$this->obj->getLastModified('U'); ?>"></input>
<div class="status" style="display:none;"></div>
<div class="playlist_title">
<h3>
<a id="ws_name" contenteditable="true"><?php echo $form->getElement("name")->getValue() ?></a>
</h3>
<h4 id="ws_length">00h 00m</h4>
</div>
<fieldset class="toggle" id="fieldset-metadate_change">
<legend style="cursor: pointer;"><span class="ui-icon ui-icon-triangle-2-n-s"></span><?php echo _("View / edit description"); ?></legend>
<dl class="zend_form">
<dt>
<label><?php echo _("Description") ?></label>
</dt>
<dd>
<textarea cols="80" rows="24" id="ws_description"><?php echo $form->getElement("description")->getValue() ?></textarea>
</dd>
</dl>
</fieldset>
<dl class="zend_form">
<dt>
<label><?php echo _("Stream URL:"); ?></label>
</dt>
<dd>
<input id="ws_url" type="text" value="<?php echo $form->getElement("url")->getValue() ?>" size="60"/>
</dd>
<dt>
<label><?php echo _("Default Length:"); ?></label>
</dt>
<dd>
<input id="ws_hours" type="text" value="<?php echo $form->getElement("hours")->getValue() ?>" size="2"/><?php echo _("h"); ?>
<input id="ws_mins" type="text" value="<?php echo $form->getElement("mins")->getValue() ?>" size="2"/><?php echo _("m"); ?>
</dd>
</dl>
<?php else : ?>
<div><?php echo _("No webstream") ?></div>
<?php endif; ?>

View File

@ -780,27 +780,37 @@ var AIRTIME = (function(AIRTIME){
disableLoadingIcon();
setTimeout(removeSuccessMsg, 5000);
});
})
});
$pl.on("click", "#webstream_save", function(){
$pl.on("click", "#webstream_save", function() {
//get all fields and POST to server
//description
//stream url
//default_length
//playlist name
var id = $pl.find("#obj_id").attr("value");
var description = $pl.find("#description").val();
var streamurl = $pl.find("#streamurl-element input").val();
var length = $pl.find("#streamlength-element input").val();
var name = $pl.find("#playlist_name_display").text();
var id = $pl.find("#ws_id").attr("value");
var description = $pl.find("#ws_description").val();
var streamurl = $pl.find("#ws_url").val();
var hours = $pl.find("#ws_hours").val();
var mins = $pl.find("#ws_mins").val();
var name = $pl.find("#ws_name").text();
var parameters = {
format: "json",
description: description,
url: streamurl,
hours: hours,
mins: mins,
name: name
};
if (id !== "") {
parameters["id"] = id;
}
//hide any previous errors (if any)
$("#side_playlist .errors").empty().hide();
var url = baseUrl+'Webstream/save';
$.post(url,
{format: "json", id:id, description: description, url:streamurl, length: length, name: name},
function(json){
parameters,
function(json) {
if (json.analysis){
for (var s in json.analysis){
var field = json.analysis[s];
@ -810,7 +820,8 @@ var AIRTIME = (function(AIRTIME){
var $div = $("#side_playlist " + elemId).text(field[1]).show();
}
}
} else {
}
else {
var $status = $("#side_playlist .status");
$status.html(json.statusMessage);
$status.show();
@ -828,11 +839,8 @@ var AIRTIME = (function(AIRTIME){
//redraw the library to show the new webstream
redrawLib();
}
}
});
});
$lib.on("click", "#pl_edit", function() {