Merge branch 'devel' of dev.sourcefabric.org:airtime into devel

This commit is contained in:
denise 2012-09-10 14:16:52 -04:00
commit 60cf74a602
25 changed files with 332 additions and 243 deletions

View File

@ -336,7 +336,7 @@ class ApiController extends Zend_Controller_Action
$this->view->layout()->disableLayout(); $this->view->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true); $this->_helper->viewRenderer->setNoRender(true);
$data = Application_Model_Schedule::GetScheduledPlaylists(); $data = Application_Model_Schedule::getSchedule();
echo json_encode($data, JSON_FORCE_OBJECT); echo json_encode($data, JSON_FORCE_OBJECT);
} }

View File

@ -274,7 +274,7 @@ class ShowbuilderController extends Zend_Controller_Action
$opts = array("myShows" => $my_shows, "showFilter" => $show_filter); $opts = array("myShows" => $my_shows, "showFilter" => $show_filter);
$showBuilder = new Application_Model_ShowBuilder($startsDT, $endsDT, $opts); $showBuilder = new Application_Model_ShowBuilder($startsDT, $endsDT, $opts);
$data = $showBuilder->GetItems(); $data = $showBuilder->getItems();
$this->view->schedule = $data["schedule"]; $this->view->schedule = $data["schedule"];
$this->view->instances = $data["showInstances"]; $this->view->instances = $data["showInstances"];
$this->view->timestamp = $current_time; $this->view->timestamp = $current_time;

View File

@ -5,7 +5,7 @@ class RabbitMqPlugin extends Zend_Controller_Plugin_Abstract
public function dispatchLoopShutdown() public function dispatchLoopShutdown()
{ {
if (Application_Model_RabbitMq::$doPush) { if (Application_Model_RabbitMq::$doPush) {
$md = array('schedule' => Application_Model_Schedule::GetScheduledPlaylists()); $md = array('schedule' => Application_Model_Schedule::getSchedule());
Application_Model_RabbitMq::SendMessageToPypo("update_schedule", $md); Application_Model_RabbitMq::SendMessageToPypo("update_schedule", $md);
if (!isset($_SERVER['AIRTIME_SRV'])) { if (!isset($_SERVER['AIRTIME_SRV'])) {
Application_Model_RabbitMq::SendMessageToShowRecorder("update_recorder_schedule"); Application_Model_RabbitMq::SendMessageToShowRecorder("update_recorder_schedule");

View File

@ -132,7 +132,6 @@ class Application_Model_Preference
public static function GetHeadTitle() public static function GetHeadTitle()
{ {
$title = self::getValue("station_name"); $title = self::getValue("station_name");
$defaultNamespace->title = $title;
if (strlen($title) > 0) if (strlen($title) > 0)
$title .= " - "; $title .= " - ";

View File

@ -511,7 +511,7 @@ SQL;
* Returns null if nothing found, else an array of associative * Returns null if nothing found, else an array of associative
* arrays representing each row. * arrays representing each row.
*/ */
public static function GetItems($p_startTime, $p_endTime) public static function getItems($p_startTime, $p_endTime)
{ {
global $CC_CONFIG; global $CC_CONFIG;
@ -570,7 +570,127 @@ SQL;
return $rows; return $rows;
} }
public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null) private static function createInputHarborKickTimes(&$data, $range_start, $range_end)
{
$utcTimeZone = new DateTimeZone("UTC");
$kick_times = Application_Model_ShowInstance::GetEndTimeOfNextShowWithLiveDJ($range_start, $range_end);
foreach ($kick_times as $kick_time_info) {
$kick_time = $kick_time_info['ends'];
$temp = explode('.', Application_Model_Preference::GetDefaultTransitionFade());
// we round down transition time since PHP cannot handle millisecond. We need to
// handle this better in the future
$transition_time = intval($temp[0]);
$switchOffDataTime = new DateTime($kick_time, $utcTimeZone);
$switch_off_time = $switchOffDataTime->sub(new DateInterval('PT'.$transition_time.'S'));
$switch_off_time = $switch_off_time->format("Y-m-d H:i:s");
$kick_start = self::AirtimeTimeToPypoTime($kick_time);
$data["media"][$kick_start]['start'] = $kick_start;
$data["media"][$kick_start]['end'] = $kick_start;
$data["media"][$kick_start]['event_type'] = "kick_out";
$data["media"][$kick_start]['type'] = "event";
$data["media"][$kick_start]['independent_event'] = true;
if ($kick_time !== $switch_off_time) {
$switch_start = self::AirtimeTimeToPypoTime($switch_off_time);
$data["media"][$switch_start]['start'] = $switch_start;
$data["media"][$switch_start]['end'] = $switch_start;
$data["media"][$switch_start]['event_type'] = "switch_off";
$data["media"][$switch_start]['independent_event'] = true;
}
}
}
private static function createFileScheduleEvent(&$data, $item, $media_id)
{
$start = self::AirtimeTimeToPypoTime($item["start"]);
$end = self::AirtimeTimeToPypoTime($item["end"]);
$schedule_item = array(
'id' => $media_id,
'type' => 'file',
'row_id' => $item["id"],
'uri' => $uri,
'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]),
'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]),
'cue_in' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_in"]),
'cue_out' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_out"]),
'start' => $start,
'end' => $end,
'show_name' => $showName,
'replay_gain' => is_null($item["replay_gain"]) ? "0": $item["replay_gain"],
'independent_event' => true
);
$data["media"][$start] = $schedule_item;
}
private static function createStreamScheduleEvent(&$data, $item, $media_id)
{
$start = self::AirtimeTimeToPypoTime($item["start"]);
$end = self::AirtimeTimeToPypoTime($item["end"]);
//create an event to start stream buffering 5 seconds ahead of the streams actual time.
$buffer_start = new DateTime($item["start"], new DateTimeZone('UTC'));
$buffer_start->sub(new DateInterval("PT5S"));
$stream_buffer_start = self::AirtimeTimeToPypoTime($buffer_start->format("Y-m-d H:i:s"));
$schedule_item = array(
'start' => $stream_buffer_start,
'end' => $stream_buffer_start,
'uri' => $uri,
'row_id' => $item["id"],
'type' => 'stream_buffer_start',
'independent_event' => true
);
//TODO: Make sure no other media is being overwritten!
$data["media"][$stream_buffer_start] = $schedule_item;
$schedule_item = array(
'id' => $media_id,
'type' => 'stream_output_start',
'row_id' => $item["id"],
'uri' => $uri,
'start' => $start,
'end' => $end,
'show_name' => $showName,
'independent_event' => true
);
$data["media"][$start] = $schedule_item;
//since a stream never ends we have to insert an additional "kick stream" event. The "start"
//time of this event is the "end" time of the stream minus 1 second.
$dt = new DateTime($item["end"], new DateTimeZone('UTC'));
$dt->sub(new DateInterval("PT1S"));
//make sure the webstream doesn't play past the end time of the show
if ($dt->getTimestamp() > $showEndDateTime->getTimestamp()) {
$dt = $showEndDateTime;
}
$stream_end = self::AirtimeTimeToPypoTime($dt->format("Y-m-d H:i:s"));
$schedule_item = array(
'start' => $stream_end,
'end' => $stream_end,
'uri' => $uri,
'type' => 'stream_buffer_end',
'independent_event' => true
);
$data["media"][$stream_end] = $schedule_item;
$schedule_item = array(
'start' => $stream_end,
'end' => $stream_end,
'uri' => $uri,
'type' => 'stream_output_end',
'independent_event' => true
);
$data["media"][$stream_end] = $schedule_item;
}
private static function getRangeStartAndEnd($p_fromDateTime, $p_toDateTime)
{ {
global $CC_CONFIG; global $CC_CONFIG;
@ -600,8 +720,13 @@ SQL;
$range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime); $range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime);
} }
// Scheduler wants everything in a playlist return array($range_start, $range_end);
$items = self::GetItems($range_start, $range_end); }
public static function getSchedule($p_fromDateTime = null, $p_toDateTime = null)
{
list($range_start, $range_end) = self::getRangeStartAndEnd($p_fromDateTime, $p_toDateTime);
$data = array(); $data = array();
$utcTimeZone = new DateTimeZone("UTC"); $utcTimeZone = new DateTimeZone("UTC");
@ -609,33 +734,9 @@ SQL;
$data["status"] = array(); $data["status"] = array();
$data["media"] = array(); $data["media"] = array();
$kick_times = Application_Model_ShowInstance::GetEndTimeOfNextShowWithLiveDJ($range_start, $range_end); self::createInputHarborKickTimes($data, $range_start, $range_end);
foreach ($kick_times as $kick_time_info) {
$kick_time = $kick_time_info['ends'];
$temp = explode('.', Application_Model_Preference::GetDefaultTransitionFade());
// we round down transition time since PHP cannot handle millisecond. We need to
// handle this better in the future
$transition_time = intval($temp[0]);
$switchOffDataTime = new DateTime($kick_time, $utcTimeZone);
$switch_off_time = $switchOffDataTime->sub(new DateInterval('PT'.$transition_time.'S'));
$switch_off_time = $switch_off_time->format("Y-m-d H:i:s");
$kick_start = Application_Model_Schedule::AirtimeTimeToPypoTime($kick_time);
$data["media"][$kick_start]['start'] = $kick_start;
$data["media"][$kick_start]['end'] = $kick_start;
$data["media"][$kick_start]['event_type'] = "kick_out";
$data["media"][$kick_start]['type'] = "event";
$data["media"][$kick_start]['independent_event'] = true;
if ($kick_time !== $switch_off_time) {
$switch_start = Application_Model_Schedule::AirtimeTimeToPypoTime($switch_off_time);
$data["media"][$switch_start]['start'] = $switch_start;
$data["media"][$switch_start]['end'] = $switch_start;
$data["media"][$switch_start]['event_type'] = "switch_off";
$data["media"][$switch_start]['independent_event'] = true;
}
}
$items = self::getItems($range_start, $range_end);
foreach ($items as $item) { foreach ($items as $item) {
$showInstance = CcShowInstancesQuery::create()->findPK($item["instance_id"]); $showInstance = CcShowInstancesQuery::create()->findPK($item["instance_id"]);
@ -648,11 +749,10 @@ SQL;
$trackEndDateTime = new DateTime($item["end"], $utcTimeZone); $trackEndDateTime = new DateTime($item["end"], $utcTimeZone);
if ($trackStartDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) { if ($trackStartDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) {
//do not send any tracks that start past their show's end time
continue; continue;
} }
/* Note: cue_out and end are always the same. */
/* TODO: Not all tracks will have "show_end" */
if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) { if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) {
$di = $trackStartDateTime->diff($showEndDateTime); $di = $trackStartDateTime->diff($showEndDateTime);
@ -665,78 +765,37 @@ SQL;
$media_id = $item['file_id']; $media_id = $item['file_id'];
$storedFile = Application_Model_StoredFile::Recall($media_id); $storedFile = Application_Model_StoredFile::Recall($media_id);
$uri = $storedFile->getFilePath(); $uri = $storedFile->getFilePath();
$type = "file"; self::createFileScheduleEvent($data, $item, $media_id, $uri);
$independent_event = false;
} elseif (!is_null($item['stream_id'])) { } elseif (!is_null($item['stream_id'])) {
//row is type "webstream" //row is type "webstream"
$media_id = $item['stream_id']; $media_id = $item['stream_id'];
$uri = $item['url']; $uri = $item['url'];
$type = "stream"; self::createStreamScheduleEvent($data, $item, $media_id, $uri);
$independent_event = true;
}
$start = Application_Model_Schedule::AirtimeTimeToPypoTime($item["start"]);
$end = Application_Model_Schedule::AirtimeTimeToPypoTime($item["end"]);
$data["media"][$start] = array(
'id' => $media_id,
'type' => $type,
'row_id' => $item["id"],
'uri' => $uri,
'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]),
'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]),
'cue_in' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_in"]),
'cue_out' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_out"]),
'start' => $start,
'end' => $end,
'show_name' => $showName,
'replay_gain' => is_null($item["replay_gain"]) ? "0": $item["replay_gain"],
'independent_event' => $independent_event
);
if ($type == "stream") {
//create an event to start stream buffering 5 seconds ahead of the streams actual time.
$buffer_start = new DateTime($item["start"], new DateTimeZone('UTC'));
$buffer_start->sub(new DateInterval("PT5S"));
$stream_buffer_start = Application_Model_Schedule::AirtimeTimeToPypoTime($buffer_start->format("Y-m-d H:i:s"));
//TODO: Make sure no other media is being overwritten!
$data["media"][$stream_buffer_start] = array(
'start' => $stream_buffer_start,
'end' => $stream_buffer_start,
'uri' => $uri,
'row_id' => $item["id"],
'type' => 'stream_buffer_start',
'independent_event' => true
);
//since a stream never ends we have to insert an additional "kick stream" event. The "start"
//time of this event is the "end" time of the stream minus 1 second.
$dt = new DateTime($item["end"], new DateTimeZone('UTC'));
$dt->sub(new DateInterval("PT1S"));
//make sure the webstream doesn't play past the end time of the show
if ($dt->getTimestamp() > $showEndDateTime->getTimestamp()) {
$dt = $showEndDateTime;
}
$stream_end = Application_Model_Schedule::AirtimeTimeToPypoTime($dt->format("Y-m-d H:i:s"));
$data["media"][$stream_end] = array(
'start' => $stream_end,
'end' => $stream_end,
'uri' => $uri,
'type' => 'stream_end',
'independent_event' => true
);
} }
} }
return $data; return $data;
} }
/*
private static function collapseEvents($data)
{
$keys = array_keys($data);
for ($i = 0, $len = count($keys); $i < $len; $i++) {
$cur = $data[$keys[$i]];
$next = null;
if ($i+1 < $len) {
$next = $data[$keys[$i+1]];
}
if ($cur['type'] == 'stream_buffer_end' && !is_null($next) && $next['type'] == 'stream_buffer_start') {
unset($data[$keys[$i]]);
}
}
}
*/
public static function deleteAll() public static function deleteAll()
{ {
global $CC_CONFIG; global $CC_CONFIG;

View File

@ -1643,7 +1643,7 @@ SQL;
$start_string = $start_timestamp->format("Y-m-d H:i:s"); $start_string = $start_timestamp->format("Y-m-d H:i:s");
$end_string = $end_timestamp->format("Y-m-d H:i:s"); $end_string = $end_timestamp->format("Y-m-d H:i:s");
if ($onlyRecord) { if ($onlyRecord) {
$sql .= " AND (si1.starts >= :start::TIMESTAMP AND si1.starts < timestamp :end::TIMESTAMP)"; $sql .= " AND (si1.starts >= :start::TIMESTAMP AND si1.starts < :end::TIMESTAMP)";
$sql .= " AND (si1.record = 1)"; $sql .= " AND (si1.record = 1)";
return Application_Common_Database::prepareAndExecute( $sql, return Application_Common_Database::prepareAndExecute( $sql,

View File

@ -386,7 +386,7 @@ class Application_Model_ShowBuilder
return $outdated; return $outdated;
} }
public function GetItems() public function getItems()
{ {
$current_id = -1; $current_id = -1;
$display_items = array(); $display_items = array();

View File

@ -22,6 +22,8 @@ class Application_Model_ShowInstance
return $this->_showInstance->getDbShowId(); return $this->_showInstance->getDbShowId();
} }
/* TODO: A little inconsistent because other models have a getId() method
to get PK --RG */
public function getShowInstanceId() public function getShowInstanceId()
{ {
return $this->_instanceId; return $this->_instanceId;
@ -34,17 +36,17 @@ class Application_Model_ShowInstance
public function deleteRebroadcasts() public function deleteRebroadcasts()
{ {
$con = Propel::getConnection();
$timestamp = gmdate("Y-m-d H:i:s"); $timestamp = gmdate("Y-m-d H:i:s");
$instance_id = $this->getShowInstanceId(); $instance_id = $this->getShowInstanceId();
$sql = <<<SQL
$sql = "DELETE FROM cc_show_instances" DELETE FROM cc_show_instances
." WHERE starts > TIMESTAMP '$timestamp'" WHERE starts > :timestamp::TIMESTAMP
." AND instance_id = $instance_id" AND instance_id = :instanceId
." AND rebroadcast = 1"; AND rebroadcast = 1;
SQL;
$con->exec($sql); Application_Common_Database::prepareAndExecute( $sql, array(
':instanceId' => $instance_id,
':timestamp' => $timestamp), 'execute');
} }
/* This function is weird. It should return a boolean, but instead returns /* This function is weird. It should return a boolean, but instead returns
@ -167,26 +169,33 @@ class Application_Model_ShowInstance
$con = Propel::getConnection(); $con = Propel::getConnection();
$instance_id = $this->getShowInstanceId(); $instance_id = $this->getShowInstanceId();
$sql = "SELECT starts from cc_schedule" $sql = <<<SQL
." WHERE instance_id = $instance_id" SELECT starts
." ORDER BY starts" FROM cc_schedule
." LIMIT 1"; WHERE instance_id = :instanceId
ORDER BY starts LIMIT 1;
$scheduleStarts = $con->query($sql)->fetchColumn(0); SQL;
$scheduleStarts = Application_Common_Database::prepareAndExecute( $sql,
array( ':instanceId' => $instance_id ), 'column' );
if ($scheduleStarts) { if ($scheduleStarts) {
$scheduleStartsEpoch = strtotime($scheduleStarts); $scheduleStartsEpoch = strtotime($scheduleStarts);
$showStartsEpoch = strtotime($this->getShowInstanceStart()); $showStartsEpoch = strtotime($this->getShowInstanceStart());
$diff = $showStartsEpoch - $scheduleStartsEpoch; $diff = $showStartsEpoch - $scheduleStartsEpoch;
if ($diff != 0) { if ($diff != 0) {
$sql = "UPDATE cc_schedule" $sql = <<<SQL
." SET starts = starts + INTERVAL '$diff' second," UPDATE cc_schedule
." ends = ends + INTERVAL '$diff' second" SET starts = starts + INTERVAL :diff1 SECOND,
." WHERE instance_id = $instance_id"; ends = ends + INTERVAL :diff2 SECOND
WHERE instance_id = :instanceId
$con->exec($sql); SQL;
Application_Common_Database::prepareAndExecute($sql,
array(
':diff1' => $diff,
':diff2' => $diff,
':instanceId' => $instance_id ), 'execute');
} }
} }
Application_Model_RabbitMq::PushSchedule(); Application_Model_RabbitMq::PushSchedule();
@ -315,11 +324,6 @@ class Application_Model_ShowInstance
Application_Model_RabbitMq::PushSchedule(); Application_Model_RabbitMq::PushSchedule();
} }
/*
* FUNCTION SHOULD NOT BE CALLED
* - we are removing ability to resize just a single show instance
* -please use the resize method on the Show.php class.
*/
public function resizeShow($deltaDay, $deltaMin) public function resizeShow($deltaDay, $deltaMin)
{ {
$con = Propel::getConnection(); $con = Propel::getConnection();
@ -356,6 +360,7 @@ class Application_Model_ShowInstance
$overlap = Application_Model_Show::getShows($utcStartDateTime, $utcEndDateTime); $overlap = Application_Model_Show::getShows($utcStartDateTime, $utcEndDateTime);
if (count($overlap) > 0) { if (count($overlap) > 0) {
// TODO : fix ghetto error handling -- RG
return "Should not overlap shows"; return "Should not overlap shows";
} }
} }
@ -363,9 +368,17 @@ class Application_Model_ShowInstance
//must update length of all rebroadcast instances. //must update length of all rebroadcast instances.
if ($this->isRecorded()) { if ($this->isRecorded()) {
$sql = "UPDATE cc_show_instances SET ends = (ends + interval '{$deltaDay} days' + interval '{$hours}:{$mins}') $sql = <<<SQL
WHERE rebroadcast = 1 AND instance_id = {$this->_instanceId}"; UPDATE cc_show_instances
$con->exec($sql); SET ends = (ends + interval :deltaDays + interval :interval)
WHERE rebroadcast = 1
AND instance_id = :instanceId;
SQL;
Application_Common_Database::prepareAndExecute( $sql, array(
':deltaDays' => "$deltaDay days",
':interval' => "$hours:$mins",
':instanceId' => $this->_instanceId ), 'execute');
} }
$this->setShowEnd($new_ends); $this->setShowEnd($new_ends);
@ -745,102 +758,90 @@ SQL;
public static function GetLastShowInstance($p_timeNow) public static function GetLastShowInstance($p_timeNow)
{ {
global $CC_CONFIG; $sql = <<<SQL
$con = Propel::getConnection(); SELECT si.id
FROM cc_show_instances si
WHERE si.ends < :timeNow::TIMESTAMP
AND si.modified_instance = 'f'
ORDER BY si.ends DESC LIMIT 1;
SQL;
$id = Application_Common_Database( $sql, array(
':timeNow' => $p_timeNow ), 'column' );
$sql = "SELECT si.id" return ($id ? new Application_Model_ShowInstance($id) : null );
." FROM $CC_CONFIG[showInstances] si"
." WHERE si.ends < TIMESTAMP '$p_timeNow'"
." AND si.modified_instance = 'f'"
." ORDER BY si.ends DESC"
." LIMIT 1";
$id = $con->query($sql)->fetchColumn(0);
if ($id) {
return new Application_Model_ShowInstance($id);
} else {
return null;
}
} }
public static function GetCurrentShowInstance($p_timeNow) public static function GetCurrentShowInstance($p_timeNow)
{ {
global $CC_CONFIG;
$con = Propel::getConnection();
/* Orderby si.starts descending, because in some cases /* Orderby si.starts descending, because in some cases
* we can have multiple shows overlapping each other. In * we can have multiple shows overlapping each other. In
* this case, the show that started later is the one that * this case, the show that started later is the one that
* is actually playing, and so this is the one we want. * is actually playing, and so this is the one we want.
*/ */
$sql = "SELECT si.id" $sql = <<<SQL
." FROM $CC_CONFIG[showInstances] si" SELECT si.id
." WHERE si.starts <= TIMESTAMP '$p_timeNow'" FROM cc_show_instances si
." AND si.ends > TIMESTAMP '$p_timeNow'" WHERE si.starts <= :timeNow1::TIMESTAMP
." AND si.modified_instance = 'f'" AND si.ends > :timeNow2::TIMESTAMP
." ORDER BY si.starts DESC" AND si.modified_instance = 'f'
." LIMIT 1"; ORDER BY si.starts DESC LIMIT 1
SQL;
$id = $con->query($sql)->fetchColumn(0); $id = Application_Common_Database( $sql, array(
if ($id) { ':timeNow1' => $p_timeNow,
return new Application_Model_ShowInstance($id); ':timeNow2' => $p_timeNow ), 'column');
} else {
return null; return ( $id ? new Application_Model_ShowInstance($id) : null );
}
} }
public static function GetNextShowInstance($p_timeNow) public static function GetNextShowInstance($p_timeNow)
{ {
global $CC_CONFIG; $sql = <<<SQL
$con = Propel::getConnection(); SELECT si.id
FROM cc_show_instances si
$sql = "SELECT si.id" WHERE si.starts > :timeNow::TIMESTAMP
." FROM $CC_CONFIG[showInstances] si" AND si.modified_instance = 'f'
." WHERE si.starts > TIMESTAMP '$p_timeNow'" ORDER BY si.starts
." AND si.modified_instance = 'f'" LIMIT 1
." ORDER BY si.starts" SQL;
." LIMIT 1"; $id = Application_Common_Database::prepareAndExecute( $sql,
array( 'timeNow' => $p_timeNow ), 'column' );
$id = $con->query($sql)->fetchColumn(0); return ( $id ? new Application_Model_ShowInstance($id) : null );
if ($id) {
return new Application_Model_ShowInstance($id);
} else {
return null;
}
} }
// returns number of show instances that ends later than $day // returns number of show instances that ends later than $day
public static function GetShowInstanceCount($day) public static function GetShowInstanceCount($day)
{ {
global $CC_CONFIG; $sql = <<<SQL
$con = Propel::getConnection(); SELECT count(*) AS cnt
$sql = "SELECT count(*) as cnt FROM $CC_CONFIG[showInstances] WHERE ends < '$day'"; FROM cc_show_instances
WHERE ends < :day
return $con->query($sql)->fetchColumn(0); SQL;
return Application_Common_Database::prepareAndExecute( $sql,
array( ':day' => $day ), 'column' );
} }
// this returns end timestamp of all shows that are in the range and has live DJ set up // this returns end timestamp of all shows that are in the range and has live DJ set up
public static function GetEndTimeOfNextShowWithLiveDJ($p_startTime, $p_endTime) public static function GetEndTimeOfNextShowWithLiveDJ($p_startTime, $p_endTime)
{ {
global $CC_CONFIG; $sql = <<<SQL
$con = Propel::getConnection(); SELECT ends
FROM cc_show_instances AS si
$sql = "SELECT ends JOIN cc_show AS sh ON si.show_id = sh.id
FROM cc_show_instances as si WHERE si.ends > :startTime::TIMESTAMP
JOIN cc_show as sh ON si.show_id = sh.id AND si.ends < :endTime::TIMESTAMP
WHERE si.ends > '$p_startTime' and si.ends < '$p_endTime' and (sh.live_stream_using_airtime_auth or live_stream_using_custom_auth) AND (sh.live_stream_using_airtime_auth
ORDER BY si.ends"; OR live_stream_using_custom_auth)
ORDER BY si.ends
return $con->query($sql)->fetchAll(); SQL;
return Application_Common_Database::prepareAndExecute( $sql, array(
':startTime' => $p_startTime,
':endTime' => $p_endTime), 'all');
} }
public function isRepeating() public function isRepeating()
{ {
if ($this->getShow()->isRepeating()) { return $this->getShow()->isRepeating();
return true;
} else {
return false;
}
} }
} }

View File

@ -652,7 +652,7 @@ class Application_Model_StoredFile
$displayColumns = array("id", "track_title", "artist_name", "album_title", "genre", "length", $displayColumns = array("id", "track_title", "artist_name", "album_title", "genre", "length",
"year", "utime", "mtime", "ftype", "track_number", "mood", "bpm", "composer", "info_url", "year", "utime", "mtime", "ftype", "track_number", "mood", "bpm", "composer", "info_url",
"bit_rate", "sample_rate", "isrc_number", "encoded_by", "label", "copyright", "mime", "bit_rate", "sample_rate", "isrc_number", "encoded_by", "label", "copyright", "mime",
"language", "filepath","owner","conductor" "language", "filepath", "owner", "conductor", "replay_gain"
); );
//Logging::info($datatables); //Logging::info($datatables);

View File

@ -178,10 +178,9 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
if (is_null($mime)) { if (is_null($mime)) {
throw new Exception("No MIME type found for webstream."); throw new Exception("No MIME type found for webstream.");
} }
//TODO: return url
$mediaUrl = self::getMediaUrl($url, $mime, $content_length_found); $mediaUrl = self::getMediaUrl($url, $mime, $content_length_found);
if (preg_match("/(x-mpegurl)|(xspf\+xml)/", $mime)) { if (preg_match("/(x-mpegurl)|(xspf\+xml)|(pls\+xml)/", $mime)) {
list($mime, $content_length_found) = self::discoverStreamMime($mediaUrl); list($mime, $content_length_found) = self::discoverStreamMime($mediaUrl);
} }
} catch (Exception $e) { } catch (Exception $e) {
@ -222,7 +221,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
} }
private static function getXspfUrl($url) private static function getUrlData($url)
{ {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
@ -233,10 +232,17 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
//TODO: What if invalid url? //TODO: What if invalid url?
$content = curl_exec($ch); $content = curl_exec($ch);
Logging::info($content); Logging::debug($content);
// close cURL resource, and free up system resources // close cURL resource, and free up system resources
curl_close($ch); curl_close($ch);
return $content;
}
private static function getXspfUrl($url)
{
$content = self::getUrlData($url);
$dom = new DOMDocument; $dom = new DOMDocument;
//TODO: What if invalid xml? //TODO: What if invalid xml?
@ -252,19 +258,23 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
throw new Exception("Could not parse XSPF playlist"); throw new Exception("Could not parse XSPF playlist");
} }
private static function getPlsUrl($url)
{
$content = self::getUrlData($url);
$ini = parse_ini_string($content, true);
if ($ini !== false && isset($ini["playlist"]) && isset($ini["playlist"]["File1"])) {
return $ini["playlist"]["File1"];
}
throw new Exception("Could not parse PLS playlist");
}
private static function getM3uUrl($url) private static function getM3uUrl($url)
{ {
$ch = curl_init(); $content = self::getUrlData($url);
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);
Logging::info($content);
curl_close($ch);
//split into lines: //split into lines:
$delim = "\n"; $delim = "\n";
@ -288,6 +298,8 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
$media_url = self::getM3uUrl($url); $media_url = self::getM3uUrl($url);
} elseif (preg_match("/xspf\+xml/", $mime)) { } elseif (preg_match("/xspf\+xml/", $mime)) {
$media_url = self::getXspfUrl($url); $media_url = self::getXspfUrl($url);
} elseif (preg_match("/pls\+xml/", $mime)) {
$media_url = self::getPlsUrl($url);
} elseif (preg_match("/(mpeg|ogg)/", $mime)) { } elseif (preg_match("/(mpeg|ogg)/", $mime)) {
if ($content_length_found) { if ($content_length_found) {
throw new Exception("Invalid webstream - This appears to be a file download."); throw new Exception("Invalid webstream - This appears to be a file download.");
@ -314,8 +326,6 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
} }
if (preg_match("/^content-length:/i", $h)) { if (preg_match("/^content-length:/i", $h)) {
$content_length_found = true; $content_length_found = true;
//if content-length appears, this is not a web stream!!!!
//Aborting the save process.
} }
} }

View File

@ -100,7 +100,7 @@ class CcFilesTableMap extends TableMap {
$this->addColumn('SOUNDCLOUD_ERROR_MSG', 'DbSoundcloudErrorMsg', 'VARCHAR', false, 512, null); $this->addColumn('SOUNDCLOUD_ERROR_MSG', 'DbSoundcloudErrorMsg', 'VARCHAR', false, 512, null);
$this->addColumn('SOUNDCLOUD_LINK_TO_FILE', 'DbSoundcloudLinkToFile', 'VARCHAR', false, 4096, null); $this->addColumn('SOUNDCLOUD_LINK_TO_FILE', 'DbSoundcloudLinkToFile', 'VARCHAR', false, 4096, null);
$this->addColumn('SOUNDCLOUD_UPLOAD_TIME', 'DbSoundCloundUploadTime', 'TIMESTAMP', false, 6, null); $this->addColumn('SOUNDCLOUD_UPLOAD_TIME', 'DbSoundCloundUploadTime', 'TIMESTAMP', false, 6, null);
$this->addColumn('REPLAY_GAIN', 'DbReplayGain', 'VARCHAR', false, 16, null); $this->addColumn('REPLAY_GAIN', 'DbReplayGain', 'NUMERIC', false, null, null);
$this->addForeignKey('OWNER_ID', 'DbOwnerId', 'INTEGER', 'cc_subjs', 'ID', false, null, null); $this->addForeignKey('OWNER_ID', 'DbOwnerId', 'INTEGER', 'cc_subjs', 'ID', false, null, null);
// validators // validators
} // initialize() } // initialize()

View File

@ -1892,20 +1892,29 @@ abstract class BaseCcFilesQuery extends ModelCriteria
/** /**
* Filter the query on the replay_gain column * Filter the query on the replay_gain column
* *
* @param string $dbReplayGain The value to use as filter. * @param string|array $dbReplayGain The value to use as filter.
* Accepts wildcards (* and % trigger a LIKE) * Accepts an associative array('min' => $minValue, 'max' => $maxValue)
* @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
* *
* @return CcFilesQuery The current query, for fluid interface * @return CcFilesQuery The current query, for fluid interface
*/ */
public function filterByDbReplayGain($dbReplayGain = null, $comparison = null) public function filterByDbReplayGain($dbReplayGain = null, $comparison = null)
{ {
if (null === $comparison) { if (is_array($dbReplayGain)) {
if (is_array($dbReplayGain)) { $useMinMax = false;
if (isset($dbReplayGain['min'])) {
$this->addUsingAlias(CcFilesPeer::REPLAY_GAIN, $dbReplayGain['min'], Criteria::GREATER_EQUAL);
$useMinMax = true;
}
if (isset($dbReplayGain['max'])) {
$this->addUsingAlias(CcFilesPeer::REPLAY_GAIN, $dbReplayGain['max'], Criteria::LESS_EQUAL);
$useMinMax = true;
}
if ($useMinMax) {
return $this;
}
if (null === $comparison) {
$comparison = Criteria::IN; $comparison = Criteria::IN;
} elseif (preg_match('/[\%\*]/', $dbReplayGain)) {
$dbReplayGain = str_replace('*', '%', $dbReplayGain);
$comparison = Criteria::LIKE;
} }
} }
return $this->addUsingAlias(CcFilesPeer::REPLAY_GAIN, $dbReplayGain, $comparison); return $this->addUsingAlias(CcFilesPeer::REPLAY_GAIN, $dbReplayGain, $comparison);

View File

@ -104,7 +104,7 @@ class SchedulerTests extends PHPUnit_TestCase {
$groupId1 = $i1->add('2008-01-01 12:00:00.000', $this->storedFile->getId()); $groupId1 = $i1->add('2008-01-01 12:00:00.000', $this->storedFile->getId());
$i2 = new Application_Model_ScheduleGroup(); $i2 = new Application_Model_ScheduleGroup();
$i2->addAfter($groupId1, $this->storedFile->getId()); $i2->addAfter($groupId1, $this->storedFile->getId());
$items = Application_Model_Schedule::GetItems("2008-01-01", "2008-01-02"); $items = Application_Model_Schedule::getItems("2008-01-01", "2008-01-02");
if (count($items) != 2) { if (count($items) != 2) {
$this->fail("Wrong number of items returned."); $this->fail("Wrong number of items returned.");
return; return;

View File

@ -99,7 +99,7 @@ while ($showTime < $endDate) {
} }
if (Application_Model_RabbitMq::$doPush) { if (Application_Model_RabbitMq::$doPush) {
$md = array('schedule' => Application_Model_Schedule::GetScheduledPlaylists()); $md = array('schedule' => Application_Model_Schedule::getSchedule());
Application_Model_RabbitMq::SendMessageToPypo("update_schedule", $md); Application_Model_RabbitMq::SendMessageToPypo("update_schedule", $md);
} }

View File

@ -74,7 +74,7 @@
<column name="soundcloud_error_msg" phpName="DbSoundcloudErrorMsg" type="VARCHAR" size="512" required="false"/> <column name="soundcloud_error_msg" phpName="DbSoundcloudErrorMsg" type="VARCHAR" size="512" required="false"/>
<column name="soundcloud_link_to_file" phpName="DbSoundcloudLinkToFile" type="VARCHAR" size="4096" required="false"/> <column name="soundcloud_link_to_file" phpName="DbSoundcloudLinkToFile" type="VARCHAR" size="4096" required="false"/>
<column name="soundcloud_upload_time" phpName="DbSoundCloundUploadTime" type="TIMESTAMP" size="6" required="false"/> <column name="soundcloud_upload_time" phpName="DbSoundCloundUploadTime" type="TIMESTAMP" size="6" required="false"/>
<column name="replay_gain" phpName="DbReplayGain" type="VARCHAR" size="16" required="false"/> <column name="replay_gain" phpName="DbReplayGain" type="NUMERIC" required="false"/>
<column name="owner_id" phpName="DbOwnerId" type="INTEGER" required="false"/> <column name="owner_id" phpName="DbOwnerId" type="INTEGER" required="false"/>
<foreign-key foreignTable="cc_subjs" phpName="FkOwner" name="cc_files_owner_fkey"> <foreign-key foreignTable="cc_subjs" phpName="FkOwner" name="cc_files_owner_fkey">
<reference local="owner_id" foreign="id"/> <reference local="owner_id" foreign="id"/>

View File

@ -92,7 +92,7 @@ CREATE TABLE "cc_files"
"soundcloud_error_msg" VARCHAR(512), "soundcloud_error_msg" VARCHAR(512),
"soundcloud_link_to_file" VARCHAR(4096), "soundcloud_link_to_file" VARCHAR(4096),
"soundcloud_upload_time" TIMESTAMP(6), "soundcloud_upload_time" TIMESTAMP(6),
"replay_gain" VARCHAR(16), "replay_gain" NUMERIC,
"owner_id" INTEGER, "owner_id" INTEGER,
PRIMARY KEY ("id") PRIMARY KEY ("id")
); );
@ -633,7 +633,7 @@ CREATE TABLE "cc_webstream"
"id" serial NOT NULL, "id" serial NOT NULL,
"name" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL,
"description" VARCHAR(255) NOT NULL, "description" VARCHAR(255) NOT NULL,
"url" VARCHAR(255) NOT NULL, "url" VARCHAR(512) NOT NULL,
"length" interval default '00:00:00' NOT NULL, "length" interval default '00:00:00' NOT NULL,
"creator_id" INTEGER NOT NULL, "creator_id" INTEGER NOT NULL,
"mtime" TIMESTAMP(6) NOT NULL, "mtime" TIMESTAMP(6) NOT NULL,

View File

@ -36,7 +36,8 @@ var AIRTIME = (function(AIRTIME) {
"track_title" : "s", "track_title" : "s",
"track_num" : "n", "track_num" : "n",
"year" : "n", "year" : "n",
"owner" : "s" "owner" : "s",
"replay_gain" : "n"
}; };
if (AIRTIME.library === undefined) { if (AIRTIME.library === undefined) {
@ -414,7 +415,8 @@ var AIRTIME = (function(AIRTIME) {
/* Mime */ { "sTitle" : "Mime" , "mDataProp" : "mime" , "bVisible" : false , "sClass" : "library_mime" , "sWidth" : "80px" } , /* Mime */ { "sTitle" : "Mime" , "mDataProp" : "mime" , "bVisible" : false , "sClass" : "library_mime" , "sWidth" : "80px" } ,
/* Language */ { "sTitle" : "Language" , "mDataProp" : "language" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" } , /* Language */ { "sTitle" : "Language" , "mDataProp" : "language" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" } ,
/* Owner */ { "sTitle" : "Owner" , "mDataProp" : "owner" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" } , /* Owner */ { "sTitle" : "Owner" , "mDataProp" : "owner" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" } ,
/* Conductor */ { "sTitle" : "Conductor" , "mDataProp" : "conductor" , "bVisible" : false , "sClass" : "library_conductor" , "sWidth" : "125px" } /* Conductor */ { "sTitle" : "Conductor" , "mDataProp" : "conductor" , "bVisible" : false , "sClass" : "library_conductor" , "sWidth" : "125px" },
/* Replay Gain */ { "sTitle" : "Replay Gain" , "mDataProp" : "replay_gain" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" }
], ],
"bProcessing": true, "bProcessing": true,

View File

@ -52,7 +52,8 @@ php-pear php5-gd postgresql odbc-postgresql python libsoundtouch-ocaml \
libtaglib-ocaml libao-ocaml libmad-ocaml ecasound \ libtaglib-ocaml libao-ocaml libmad-ocaml ecasound \
libesd0 libportaudio2 libsamplerate0 rabbitmq-server patch \ libesd0 libportaudio2 libsamplerate0 rabbitmq-server patch \
php5-curl mpg123 monit python-virtualenv multitail libcamomile-ocaml-data \ php5-curl mpg123 monit python-virtualenv multitail libcamomile-ocaml-data \
libpulse0 vorbis-tools lsb-release lsof sudo mp3gain vorbisgain flac vorbis-tools libpulse0 vorbis-tools lsb-release lsof sudo mp3gain vorbisgain flac vorbis-tools \
pwgen
#install packages with --force-yes option (this is useful in the case #install packages with --force-yes option (this is useful in the case
#of Debian, where these packages are unauthorized) #of Debian, where these packages are unauthorized)

View File

@ -43,7 +43,8 @@ php-pear php5-gd postgresql odbc-postgresql python libsoundtouch-ocaml \
libtaglib-ocaml libao-ocaml libmad-ocaml ecasound \ libtaglib-ocaml libao-ocaml libmad-ocaml ecasound \
libesd0 libportaudio2 libsamplerate0 rabbitmq-server patch \ libesd0 libportaudio2 libsamplerate0 rabbitmq-server patch \
php5-curl mpg123 monit python-virtualenv multitail libcamomile-ocaml-data \ php5-curl mpg123 monit python-virtualenv multitail libcamomile-ocaml-data \
libpulse0 vorbis-tools lsb-release lsof sudo mp3gain vorbisgain flac vorbis-tools libpulse0 vorbis-tools lsb-release lsof sudo mp3gain vorbisgain flac vorbis-tools \
pwgen
#install packages with --force-yes option (this is useful in the case #install packages with --force-yes option (this is useful in the case
#of Debian, where these packages are unauthorized) #of Debian, where these packages are unauthorized)

View File

@ -28,8 +28,6 @@ class Manager(Loggable):
include adding watched,store, organize directories, etc. Basically include adding watched,store, organize directories, etc. Basically
composes over WatchManager from pyinotify composes over WatchManager from pyinotify
""" """
global_inst = None
all_signals = set(['add_watch', 'remove_watch'])
def __init__(self): def __init__(self):
self.wm = pyinotify.WatchManager() self.wm = pyinotify.WatchManager()
# These two instance variables are assumed to be constant # These two instance variables are assumed to be constant
@ -66,7 +64,6 @@ class Manager(Loggable):
# The following set isn't really necessary anymore. Should be # The following set isn't really necessary anymore. Should be
# removed... # removed...
self.watched_directories = set([]) self.watched_directories = set([])
Manager.global_inst = self
# This is the only event that we are unable to process "normally". I.e. # This is the only event that we are unable to process "normally". I.e.
# through dedicated handler objects. Because we must have access to a # through dedicated handler objects. Because we must have access to a

View File

@ -393,7 +393,7 @@ end
dyn_out = output.icecast(%wav, dyn_out = output.icecast(%wav,
host="localhost", host="localhost",
port=8999, port=8999,
password="hackme", password=stream_harbor_pass,
mount="test-harbor", mount="test-harbor",
fallible=true) fallible=true)
@ -406,7 +406,7 @@ end
# Function to create a playlist source and output it. # Function to create a playlist source and output it.
def create_dynamic_source(uri) = def create_dynamic_source(uri) =
# The playlist source # The playlist source
s = input.http(buffer=2., max=12., uri) s = audio_to_stereo(input.http(buffer=2., max=12., uri))
# The output # The output
active_dyn_out = dyn_out(s) active_dyn_out = dyn_out(s)

View File

@ -36,9 +36,11 @@ s2_namespace = ref ''
s3_namespace = ref '' s3_namespace = ref ''
just_switched = ref false just_switched = ref false
stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20'))
%include "ls_lib.liq" %include "ls_lib.liq"
web_stream = input.harbor("test-harbor", port=8999, password="hackme") web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass)
web_stream = on_metadata(notify_stream, web_stream) web_stream = on_metadata(notify_stream, web_stream)
queue = on_metadata(notify, queue) queue = on_metadata(notify, queue)

View File

@ -40,7 +40,7 @@ except Exception, e:
sys.exit() sys.exit()
def is_stream(media_item): def is_stream(media_item):
return media_item['type'] == 'stream' return media_item['type'] == 'stream_output_start'
def is_file(media_item): def is_file(media_item):
return media_item['type'] == 'file' return media_item['type'] == 'file'
@ -211,7 +211,7 @@ class PypoPush(Thread):
queue. queue.
""" """
file_chain = filter(lambda item: (item["type"] == "file"), current_event_chain) file_chain = filter(lambda item: (item["type"] == "file"), current_event_chain)
stream_chain = filter(lambda item: (item["type"] == "stream"), current_event_chain) stream_chain = filter(lambda item: (item["type"] == "stream_output_start"), current_event_chain)
self.logger.debug(self.current_stream_info) self.logger.debug(self.current_stream_info)
self.logger.debug(current_event_chain) self.logger.debug(current_event_chain)
@ -427,13 +427,13 @@ class PypoPush(Thread):
PypoFetch.switch_source(self.logger, self.telnet_lock, "live_dj", "off") PypoFetch.switch_source(self.logger, self.telnet_lock, "live_dj", "off")
elif media_item['type'] == 'stream_buffer_start': elif media_item['type'] == 'stream_buffer_start':
self.start_web_stream_buffer(media_item) self.start_web_stream_buffer(media_item)
elif media_item['type'] == "stream": elif media_item['type'] == "stream_output_start":
if media_item['row_id'] != self.current_prebuffering_stream_id: if media_item['row_id'] != self.current_prebuffering_stream_id:
#this is called if the stream wasn't scheduled sufficiently ahead of time #this is called if the stream wasn't scheduled sufficiently ahead of time
#so that the prebuffering stage could take effect. Let's do the prebuffering now. #so that the prebuffering stage could take effect. Let's do the prebuffering now.
self.start_web_stream_buffer(media_item) self.start_web_stream_buffer(media_item)
self.start_web_stream(media_item) self.start_web_stream(media_item)
elif media_item['type'] == "stream_end": elif media_item['type'] == "stream_buffer_end":
self.stop_web_stream(media_item) self.stop_web_stream(media_item)
except Exception, e: except Exception, e:
self.logger.error('Pypo Push Exception: %s', e) self.logger.error('Pypo Push Exception: %s', e)

View File

@ -78,7 +78,7 @@ echo "Removing everything from the scheduler between $startTime and $endTime..."
$scheduleClear = Schedule::isScheduleEmptyInRange($startTime, "01:00:00"); $scheduleClear = Schedule::isScheduleEmptyInRange($startTime, "01:00:00");
if (!$scheduleClear) { if (!$scheduleClear) {
echo "\nERROR: Schedule could not be cleared.\n\n"; echo "\nERROR: Schedule could not be cleared.\n\n";
var_dump(Schedule::GetItems($startTime, $endTime)); var_dump(Schedule::getItems($startTime, $endTime));
exit; exit;
} }
echo "done.\n"; echo "done.\n";

View File

@ -35,6 +35,14 @@ get_include_path(),
realpath($CC_CONFIG['phpDir'] . '/library') realpath($CC_CONFIG['phpDir'] . '/library')
))); )));
function __autoload($classname){
global $CC_CONFIG;
$info = explode('_', $classname);
if (isset($info[2])) {
$filename = $info[2].".php";
require_once($CC_CONFIG['phpDir'].'/application/models/'.$filename);
}
}
require_once($CC_CONFIG['phpDir'].'/application/models/User.php'); require_once($CC_CONFIG['phpDir'].'/application/models/User.php');
require_once($CC_CONFIG['phpDir'].'/application/models/StoredFile.php'); require_once($CC_CONFIG['phpDir'].'/application/models/StoredFile.php');
require_once($CC_CONFIG['phpDir'].'/application/models/Playlist.php'); require_once($CC_CONFIG['phpDir'].'/application/models/Playlist.php');