timezone. * Fixed GetPlayOrderRange to be consistently return everything in UTC. * ApiController liveInfoAction now returns everything consistently in the station timezone. This fixes negative remaining time that could occur in the embeddable JS Airtime widgets if you were logged in to Airtime while you viewed your website. (The widgets display in the browser's local time.) * ScheduleController getCurrentPlaylistAction() returns everything consistently in the user timezone now.
1107 lines
46 KiB
PHP
1107 lines
46 KiB
PHP
<?php
|
|
|
|
class ApiController extends Zend_Controller_Action
|
|
{
|
|
|
|
public function init()
|
|
{
|
|
$ignoreAuth = array("live-info", "week-info");
|
|
|
|
$params = $this->getRequest()->getParams();
|
|
if (!in_array($params['action'], $ignoreAuth)) {
|
|
$this->checkAuth();
|
|
}
|
|
/* Initialize action controller here */
|
|
$context = $this->_helper->getHelper('contextSwitch');
|
|
$context->addActionContext('version' , 'json')
|
|
->addActionContext('recorded-shows' , 'json')
|
|
->addActionContext('calendar-init' , 'json')
|
|
->addActionContext('upload-file' , 'json')
|
|
->addActionContext('upload-recorded' , 'json')
|
|
->addActionContext('media-monitor-setup' , 'json')
|
|
->addActionContext('media-item-status' , 'json')
|
|
->addActionContext('reload-metadata' , 'json')
|
|
->addActionContext('list-all-files' , 'json')
|
|
->addActionContext('list-all-watched-dirs' , 'json')
|
|
->addActionContext('add-watched-dir' , 'json')
|
|
->addActionContext('remove-watched-dir' , 'json')
|
|
->addActionContext('set-storage-dir' , 'json')
|
|
->addActionContext('get-stream-setting' , 'json')
|
|
->addActionContext('status' , 'json')
|
|
->addActionContext('register-component' , 'json')
|
|
->addActionContext('update-liquidsoap-status' , 'json')
|
|
->addActionContext('live-chat' , 'json')
|
|
->addActionContext('update-file-system-mount' , 'json')
|
|
->addActionContext('handle-watched-dir-missing' , 'json')
|
|
->addActionContext('rabbitmq-do-push' , 'json')
|
|
->addActionContext('check-live-stream-auth' , 'json')
|
|
->addActionContext('update-source-status' , 'json')
|
|
->addActionContext('get-bootstrap-info' , 'json')
|
|
->addActionContext('get-files-without-replay-gain' , 'json')
|
|
->addActionContext('get-files-without-silan-value' , 'json')
|
|
->addActionContext('reload-metadata-group' , 'json')
|
|
->addActionContext('notify-webstream-data' , 'json')
|
|
->addActionContext('get-stream-parameters' , 'json')
|
|
->addActionContext('push-stream-stats' , 'json')
|
|
->addActionContext('update-stream-setting-table' , 'json')
|
|
->addActionContext('update-replay-gain-value' , 'json')
|
|
->addActionContext('update-cue-values-by-silan' , 'json')
|
|
->initContext();
|
|
}
|
|
|
|
public function checkAuth()
|
|
{
|
|
$CC_CONFIG = Config::getConfig();
|
|
$api_key = $this->_getParam('api_key');
|
|
|
|
if (!in_array($api_key, $CC_CONFIG["apiKey"]) &&
|
|
is_null(Zend_Auth::getInstance()->getStorage()->read())) {
|
|
header('HTTP/1.0 401 Unauthorized');
|
|
print _('You are not allowed to access this resource.');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
public function versionAction()
|
|
{
|
|
$this->_helper->json->sendJson( array(
|
|
"version" => Application_Model_Preference::GetAirtimeVersion()));
|
|
}
|
|
|
|
/**
|
|
* Allows remote client to download requested media file.
|
|
*
|
|
* @return void
|
|
*
|
|
*/
|
|
public function getMediaAction()
|
|
{
|
|
$fileId = $this->_getParam("file");
|
|
|
|
$media = Application_Model_StoredFile::RecallById($fileId);
|
|
if ($media != null) {
|
|
|
|
$filepath = $media->getFilePath();
|
|
// Make sure we don't have some wrong result beecause of caching
|
|
clearstatcache();
|
|
if (is_file($filepath)) {
|
|
$full_path = $media->getPropelOrm()->getDbFilepath();
|
|
|
|
$file_base_name = strrchr($full_path, '/');
|
|
/* If $full_path does not contain a '/', strrchr will return false,
|
|
* in which case we can use $full_path as the base name.
|
|
*/
|
|
if (!$file_base_name) {
|
|
$file_base_name = $full_path;
|
|
} else {
|
|
$file_base_name = substr($file_base_name, 1);
|
|
}
|
|
|
|
//Download user left clicks a track and selects Download.
|
|
if ("true" == $this->_getParam('download')) {
|
|
//path_info breaks up a file path into seperate pieces of informaiton.
|
|
//We just want the basename which is the file name with the path
|
|
//information stripped away. We are using Content-Disposition to specify
|
|
//to the browser what name the file should be saved as.
|
|
header('Content-Disposition: attachment; filename="'.$file_base_name.'"');
|
|
} else {
|
|
//user clicks play button for track and downloads it.
|
|
header('Content-Disposition: inline; filename="'.$file_base_name.'"');
|
|
}
|
|
|
|
$this->smartReadFile($filepath, $media->getPropelOrm()->getDbMime());
|
|
exit;
|
|
} else {
|
|
header ("HTTP/1.1 404 Not Found");
|
|
}
|
|
}
|
|
|
|
$this->_helper->json->sendJson(array());
|
|
}
|
|
|
|
/**
|
|
* Reads the requested portion of a file and sends its contents to the client with the appropriate headers.
|
|
*
|
|
* This HTTP_RANGE compatible read file function is necessary for allowing streaming media to be skipped around in.
|
|
*
|
|
* @param string $location
|
|
* @param string $mimeType
|
|
* @return void
|
|
*
|
|
* @link https://groups.google.com/d/msg/jplayer/nSM2UmnSKKA/Hu76jDZS4xcJ
|
|
* @link http://php.net/manual/en/function.readfile.php#86244
|
|
*/
|
|
public function smartReadFile($location, $mimeType = 'audio/mp3')
|
|
{
|
|
$size= filesize($location);
|
|
$time= date('r', filemtime($location));
|
|
|
|
$fm = @fopen($location, 'rb');
|
|
if (!$fm) {
|
|
header ("HTTP/1.1 505 Internal server error");
|
|
|
|
return;
|
|
}
|
|
|
|
$begin = 0;
|
|
$end = $size - 1;
|
|
|
|
if (isset($_SERVER['HTTP_RANGE'])) {
|
|
if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
|
|
$begin = intval($matches[1]);
|
|
if (!empty($matches[2])) {
|
|
$end = intval($matches[2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_SERVER['HTTP_RANGE'])) {
|
|
header('HTTP/1.1 206 Partial Content');
|
|
} else {
|
|
header('HTTP/1.1 200 OK');
|
|
}
|
|
header("Content-Type: $mimeType");
|
|
header('Cache-Control: public, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
header('Accept-Ranges: bytes');
|
|
header('Content-Length:' . (($end - $begin) + 1));
|
|
if (isset($_SERVER['HTTP_RANGE'])) {
|
|
header("Content-Range: bytes $begin-$end/$size");
|
|
}
|
|
header("Content-Transfer-Encoding: binary");
|
|
header("Last-Modified: $time");
|
|
|
|
//We can have multiple levels of output buffering. Need to
|
|
//keep looping until all have been disabled!!!
|
|
//http://www.php.net/manual/en/function.ob-end-flush.php
|
|
while (@ob_end_flush());
|
|
|
|
$cur = $begin;
|
|
fseek($fm, $begin, 0);
|
|
|
|
while (!feof($fm) && $cur <= $end && (connection_status() == 0)) {
|
|
echo fread($fm, min(1024 * 16, ($end - $cur) + 1));
|
|
$cur += 1024 * 16;
|
|
}
|
|
}
|
|
|
|
//Used by the SaaS monitoring
|
|
public function onAirLightAction()
|
|
{
|
|
$this->view->layout()->disableLayout();
|
|
$this->_helper->viewRenderer->setNoRender(true);
|
|
|
|
$result = array();
|
|
$result["on_air_light"] = false;
|
|
$result["on_air_light_expected_status"] = false;
|
|
$result["station_down"] = false;
|
|
|
|
$range = Application_Model_Schedule::GetPlayOrderRange();
|
|
|
|
$isItemCurrentlyScheduled = !is_null($range["current"]) && count($range["currentShow"]) > 0 ? true : false;
|
|
|
|
$isCurrentItemPlaying = $range["current"]["media_item_played"] ? true : false;
|
|
|
|
if ($isItemCurrentlyScheduled ||
|
|
Application_Model_Preference::GetSourceSwitchStatus("live_dj") == "on" ||
|
|
Application_Model_Preference::GetSourceSwitchStatus("master_dj") == "on")
|
|
{
|
|
$result["on_air_light_expected_status"] = true;
|
|
}
|
|
|
|
if (($isItemCurrentlyScheduled && $isCurrentItemPlaying) ||
|
|
Application_Model_Preference::GetSourceSwitchStatus("live_dj") == "on" ||
|
|
Application_Model_Preference::GetSourceSwitchStatus("master_dj") == "on")
|
|
{
|
|
$result["on_air_light"] = true;
|
|
}
|
|
|
|
if ($result["on_air_light_expected_status"] != $result["on_air_light"]) {
|
|
$result["station_down"] = true;
|
|
}
|
|
|
|
echo isset($_GET['callback']) ? $_GET['callback'].'('.json_encode($result).')' : json_encode($result);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the currently playing show as well as upcoming shows.
|
|
* Number of shows returned and the time interval in which to
|
|
* get the next shows can be configured as GET parameters.
|
|
*
|
|
* TODO: in the future, make interval length a parameter instead of hardcode to 48
|
|
*
|
|
* Possible parameters:
|
|
* type - Can have values of "endofday" or "interval". If set to "endofday",
|
|
* the function will retrieve shows from now to end of day.
|
|
* If set to "interval", shows in the next 48 hours will be retrived.
|
|
* Default is "interval".
|
|
* limit - How many shows to retrieve
|
|
* Default is "5".
|
|
*/
|
|
public function liveInfoAction()
|
|
{
|
|
if (Application_Model_Preference::GetAllow3rdPartyApi()) {
|
|
// disable the view and the layout
|
|
$this->view->layout()->disableLayout();
|
|
$this->_helper->viewRenderer->setNoRender(true);
|
|
|
|
$utcTimeNow = gmdate("Y-m-d H:i:s");
|
|
$utcTimeEnd = ""; // if empty, getNextShows will use interval instead of end of day
|
|
|
|
$request = $this->getRequest();
|
|
$type = $request->getParam('type');
|
|
/* This is some *extremely* lazy programming that needs to bi fixed. For some reason
|
|
* we are using two entirely different codepaths for very similar functionality (type = endofday
|
|
* vs type = interval). Needs to be fixed for 2.3 - MK */
|
|
if ($type == "endofday") {
|
|
$limit = $request->getParam('limit');
|
|
if ($limit == "" || !is_numeric($limit)) {
|
|
$limit = "5";
|
|
}
|
|
|
|
// make getNextShows use end of day
|
|
$end = Application_Common_DateHelper::getTodayStationEndDateTime();
|
|
$end->setTimezone(new DateTimeZone("UTC"));
|
|
$utcTimeEnd = $end->format("Y-m-d H:i:s");
|
|
$result = array(
|
|
"env" => APPLICATION_ENV,
|
|
"schedulerTime" => $utcTimeNow,
|
|
"currentShow" => Application_Model_Show::getCurrentShow($utcTimeNow),
|
|
"nextShow" => Application_Model_Show::getNextShows($utcTimeNow, $limit, $utcTimeEnd)
|
|
);
|
|
}
|
|
else {
|
|
$result = Application_Model_Schedule::GetPlayOrderRange();
|
|
|
|
// XSS exploit prevention
|
|
$result["previous"]["name"] = htmlspecialchars($result["previous"]["name"]);
|
|
$result["current"]["name"] = htmlspecialchars($result["current"]["name"]);
|
|
$result["next"]["name"] = htmlspecialchars($result["next"]["name"]);
|
|
}
|
|
|
|
// XSS exploit prevention
|
|
foreach ($result["currentShow"] as &$current) {
|
|
$current["name"] = htmlspecialchars($current["name"]);
|
|
}
|
|
foreach ($result["nextShow"] as &$next) {
|
|
$next["name"] = htmlspecialchars($next["name"]);
|
|
}
|
|
|
|
//For consistency, all times here are being sent in the station timezone, which
|
|
//seems to be what we've normalized everything to.
|
|
|
|
//Convert the UTC scheduler time ("now") to the station timezone.
|
|
$result["schedulerTime"] = Application_Common_DateHelper::UTCStringToStationTimezoneString($result["schedulerTime"]);
|
|
$result["timezone"] = Application_Common_DateHelper::getStationTimezoneAbbreviation();
|
|
$result["timezoneOffset"] = Application_Common_DateHelper::getStationTimezoneOffset();
|
|
|
|
//Convert from UTC to station time for Web Browser.
|
|
Application_Common_DateHelper::convertTimestamps($result["currentShow"],
|
|
array("starts", "ends", "start_timestamp", "end_timestamp"),
|
|
"station");
|
|
Application_Common_DateHelper::convertTimestamps($result["nextShow"],
|
|
array("starts", "ends", "start_timestamp", "end_timestamp"),
|
|
"station");
|
|
|
|
//used by caller to determine if the airtime they are running or widgets in use is out of date.
|
|
$result['AIRTIME_API_VERSION'] = AIRTIME_API_VERSION;
|
|
header("Content-Type: application/json");
|
|
|
|
// If a callback is not given, then just provide the raw JSON.
|
|
echo isset($_GET['callback']) ? $_GET['callback'].'('.json_encode($result).')' : json_encode($result);
|
|
} else {
|
|
header('HTTP/1.0 401 Unauthorized');
|
|
print _('You are not allowed to access this resource. ');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
public function weekInfoAction()
|
|
{
|
|
if (Application_Model_Preference::GetAllow3rdPartyApi()) {
|
|
// disable the view and the layout
|
|
$this->view->layout()->disableLayout();
|
|
$this->_helper->viewRenderer->setNoRender(true);
|
|
|
|
//weekStart is in station time.
|
|
$weekStartDateTime = Application_Common_DateHelper::getWeekStartDateTime();
|
|
|
|
$dow = array("monday", "tuesday", "wednesday", "thursday", "friday",
|
|
"saturday", "sunday", "nextmonday", "nexttuesday", "nextwednesday",
|
|
"nextthursday", "nextfriday", "nextsaturday", "nextsunday");
|
|
|
|
$result = array();
|
|
$utcTimezone = new DateTimeZone("UTC");
|
|
$stationTimezone = new DateTimeZone(Application_Model_Preference::GetDefaultTimezone());
|
|
|
|
$weekStartDateTime->setTimezone($utcTimezone);
|
|
$utcDayStart = $weekStartDateTime->format("Y-m-d H:i:s");
|
|
for ($i = 0; $i < 14; $i++) {
|
|
|
|
//have to be in station timezone when adding 1 day for daylight savings.
|
|
$weekStartDateTime->setTimezone($stationTimezone);
|
|
$weekStartDateTime->add(new DateInterval('P1D'));
|
|
|
|
//convert back to UTC to get the actual timestamp used for search.
|
|
$weekStartDateTime->setTimezone($utcTimezone);
|
|
|
|
$utcDayEnd = $weekStartDateTime->format("Y-m-d H:i:s");
|
|
$shows = Application_Model_Show::getNextShows($utcDayStart, "ALL", $utcDayEnd);
|
|
$utcDayStart = $utcDayEnd;
|
|
|
|
Application_Common_DateHelper::convertTimestamps(
|
|
$shows,
|
|
array("starts", "ends", "start_timestamp","end_timestamp"),
|
|
"station"
|
|
);
|
|
|
|
$result[$dow[$i]] = $shows;
|
|
}
|
|
|
|
// XSS exploit prevention
|
|
foreach ($dow as $d) {
|
|
foreach ($result[$d] as &$show) {
|
|
$show["name"] = htmlspecialchars($show["name"]);
|
|
$show["url"] = htmlspecialchars($show["url"]);
|
|
}
|
|
}
|
|
|
|
//used by caller to determine if the airtime they are running or widgets in use is out of date.
|
|
$result['AIRTIME_API_VERSION'] = AIRTIME_API_VERSION;
|
|
header("Content-type: text/javascript");
|
|
// If a callback is not given, then just provide the raw JSON.
|
|
echo isset($_GET['callback']) ? $_GET['callback'].'('.json_encode($result).')' : json_encode($result);
|
|
} else {
|
|
header('HTTP/1.0 401 Unauthorized');
|
|
print _('You are not allowed to access this resource. ');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
public function scheduleAction()
|
|
{
|
|
$this->view->layout()->disableLayout();
|
|
$this->_helper->viewRenderer->setNoRender(true);
|
|
|
|
header("Content-Type: application/json");
|
|
|
|
$data = Application_Model_Schedule::getSchedule();
|
|
|
|
echo json_encode($data, JSON_FORCE_OBJECT);
|
|
}
|
|
|
|
public function notifyMediaItemStartPlayAction()
|
|
{
|
|
$media_id = $this->_getParam("media_id");
|
|
Logging::debug("Received notification of new media item start: $media_id");
|
|
Application_Model_Schedule::UpdateMediaPlayedStatus($media_id);
|
|
|
|
$historyService = new Application_Service_HistoryService();
|
|
$historyService->insertPlayedItem($media_id);
|
|
|
|
//set a 'last played' timestamp for media item
|
|
//needed for smart blocks
|
|
try {
|
|
$mediaType = Application_Model_Schedule::GetType($media_id);
|
|
if ($mediaType == 'file') {
|
|
$file_id = Application_Model_Schedule::GetFileId($media_id);
|
|
if (!is_null($file_id)) {
|
|
//we are dealing with a file not a stream
|
|
$file = Application_Model_StoredFile::RecallById($file_id);
|
|
$now = new DateTime("now", new DateTimeZone("UTC"));
|
|
$file->setLastPlayedTime($now);
|
|
}
|
|
} else {
|
|
// webstream
|
|
$stream_id = Application_Model_Schedule::GetStreamId($media_id);
|
|
if (!is_null($stream_id)) {
|
|
$webStream = new Application_Model_Webstream($stream_id);
|
|
$now = new DateTime("now", new DateTimeZone("UTC"));
|
|
$webStream->setLastPlayed($now);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Logging::info($e);
|
|
}
|
|
|
|
$this->_helper->json->sendJson(array("status"=>1, "message"=>""));
|
|
}
|
|
|
|
public function recordedShowsAction()
|
|
{
|
|
$utcTimezone = new DateTimeZone("UTC");
|
|
$nowDateTime = new DateTime("now", $utcTimezone);
|
|
$endDateTime = clone $nowDateTime;
|
|
$endDateTime = $endDateTime->add(new DateInterval("PT2H"));
|
|
|
|
$this->view->shows =
|
|
Application_Model_Show::getShows(
|
|
$nowDateTime,
|
|
$endDateTime,
|
|
$onlyRecord = true);
|
|
|
|
$this->view->is_recording = false;
|
|
$this->view->server_timezone = Application_Model_Preference::GetDefaultTimezone();
|
|
|
|
$rows = Application_Model_Show::getCurrentShow();
|
|
|
|
if (count($rows) > 0) {
|
|
$this->view->is_recording = ($rows[0]['record'] == 1);
|
|
}
|
|
}
|
|
|
|
public function uploadFileAction()
|
|
{
|
|
$upload_dir = ini_get("upload_tmp_dir");
|
|
$tempFilePath = Application_Model_StoredFile::uploadFile($upload_dir);
|
|
$tempFileName = basename($tempFilePath);
|
|
|
|
$fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
|
|
$result = Application_Model_StoredFile::copyFileToStor($upload_dir, $fileName, $tempFileName);
|
|
|
|
if (!is_null($result)) {
|
|
$this->_helper->json->sendJson(
|
|
array("jsonrpc" => "2.0", "error" => array("code" => $result['code'], "message" => $result['message']))
|
|
);
|
|
}
|
|
}
|
|
|
|
public function uploadRecordedAction()
|
|
{
|
|
$show_instance_id = $this->_getParam('showinstanceid');
|
|
$file_id = $this->_getParam('fileid');
|
|
$this->view->fileid = $file_id;
|
|
$this->view->showinstanceid = $show_instance_id;
|
|
$this->uploadRecordedActionParam($show_instance_id, $file_id);
|
|
}
|
|
|
|
// The paramterized version of the uploadRecordedAction controller.
|
|
// We want this controller's action to be invokable from other
|
|
// controllers instead being of only through http
|
|
public function uploadRecordedActionParam($show_instance_id, $file_id)
|
|
{
|
|
$showCanceled = false;
|
|
$file = Application_Model_StoredFile::RecallById($file_id);
|
|
//$show_instance = $this->_getParam('show_instance');
|
|
|
|
try {
|
|
$show_inst = new Application_Model_ShowInstance($show_instance_id);
|
|
$show_inst->setRecordedFile($file_id);
|
|
}
|
|
catch (Exception $e) {
|
|
//we've reached here probably because the show was
|
|
//cancelled, and therefore the show instance does not exist
|
|
//anymore (ShowInstance constructor threw this error). We've
|
|
//done all we can do (upload the file and put it in the
|
|
//library), now lets just return.
|
|
$showCanceled = true;
|
|
}
|
|
|
|
// TODO : the following is inefficient because it calls save on both
|
|
// fields
|
|
$file->setMetadataValue('MDATA_KEY_CREATOR', "Airtime Show Recorder");
|
|
$file->setMetadataValue('MDATA_KEY_TRACKNUMBER', $show_instance_id);
|
|
|
|
if (!$showCanceled && Application_Model_Preference::GetAutoUploadRecordedShowToSoundcloud()) {
|
|
$id = $file->getId();
|
|
Application_Model_Soundcloud::uploadSoundcloud($id);
|
|
}
|
|
}
|
|
|
|
public function mediaMonitorSetupAction()
|
|
{
|
|
$this->view->stor = Application_Model_MusicDir::getStorDir()->getDirectory();
|
|
|
|
$watchedDirs = Application_Model_MusicDir::getWatchedDirs();
|
|
$watchedDirsPath = array();
|
|
foreach ($watchedDirs as $wd) {
|
|
$watchedDirsPath[] = $wd->getDirectory();
|
|
}
|
|
$this->view->watched_dirs = $watchedDirsPath;
|
|
}
|
|
|
|
public function dispatchMetadata($md, $mode)
|
|
{
|
|
$return_hash = array();
|
|
Application_Model_Preference::SetImportTimestamp();
|
|
|
|
$con = Propel::getConnection(CcFilesPeer::DATABASE_NAME);
|
|
$con->beginTransaction();
|
|
try {
|
|
// create also modifies the file if it exists
|
|
if ($mode == "create") {
|
|
$filepath = $md['MDATA_KEY_FILEPATH'];
|
|
$filepath = Application_Common_OsPath::normpath($filepath);
|
|
$file = Application_Model_StoredFile::RecallByFilepath($filepath, $con);
|
|
if (is_null($file)) {
|
|
$file = Application_Model_StoredFile::Insert($md, $con);
|
|
} else {
|
|
// If the file already exists we will update and make sure that
|
|
// it's marked as 'exists'.
|
|
$file->setFileExistsFlag(true);
|
|
$file->setFileHiddenFlag(false);
|
|
$file->setMetadata($md);
|
|
}
|
|
if ($md['is_record'] != 0) {
|
|
$this->uploadRecordedActionParam($md['MDATA_KEY_TRACKNUMBER'], $file->getId());
|
|
}
|
|
|
|
} elseif ($mode == "modify") {
|
|
$filepath = $md['MDATA_KEY_FILEPATH'];
|
|
$file = Application_Model_StoredFile::RecallByFilepath($filepath, $con);
|
|
|
|
//File is not in database anymore.
|
|
if (is_null($file)) {
|
|
$return_hash['error'] = _("File does not exist in Airtime.");
|
|
}
|
|
//Updating a metadata change.
|
|
else {
|
|
//CC-5207 - restart media-monitor causes it to reevaluate all
|
|
//files in watched directories, and reset their cue-in/cue-out
|
|
//values. Since media-monitor has nothing to do with cue points
|
|
//let's unset it here. Note that on mode == "create", we still
|
|
//want media-monitor sending info about cue_out which by default
|
|
//will be equal to length of track until silan can take over.
|
|
unset($md['MDATA_KEY_CUE_IN']);
|
|
unset($md['MDATA_KEY_CUE_OUT']);
|
|
|
|
$file->setMetadata($md);
|
|
}
|
|
} elseif ($mode == "moved") {
|
|
$file = Application_Model_StoredFile::RecallByFilepath(
|
|
$md['MDATA_KEY_ORIGINAL_PATH'], $con);
|
|
|
|
if (is_null($file)) {
|
|
$return_hash['error'] = _('File does not exist in Airtime');
|
|
} else {
|
|
$filepath = $md['MDATA_KEY_FILEPATH'];
|
|
//$filepath = str_replace("\\", "", $filepath);
|
|
$file->setFilePath($filepath);
|
|
}
|
|
} elseif ($mode == "delete") {
|
|
$filepath = $md['MDATA_KEY_FILEPATH'];
|
|
$filepath = str_replace("\\", "", $filepath);
|
|
$file = Application_Model_StoredFile::RecallByFilepath($filepath, $con);
|
|
|
|
if (is_null($file)) {
|
|
$return_hash['error'] = _("File doesn't exist in Airtime.");
|
|
Logging::warn("Attempt to delete file that doesn't exist.
|
|
Path: '$filepath'");
|
|
} else {
|
|
$file->deleteByMediaMonitor();
|
|
}
|
|
} elseif ($mode == "delete_dir") {
|
|
$filepath = $md['MDATA_KEY_FILEPATH'];
|
|
//$filepath = str_replace("\\", "", $filepath);
|
|
$files = Application_Model_StoredFile::RecallByPartialFilepath($filepath, $con);
|
|
|
|
foreach ($files as $file) {
|
|
$file->deleteByMediaMonitor();
|
|
}
|
|
$return_hash['success'] = 1;
|
|
}
|
|
|
|
if (!isset($return_hash['error'])) {
|
|
$return_hash['fileid'] = is_null($file) ? '-1' : $file->getId();
|
|
}
|
|
$con->commit();
|
|
} catch (Exception $e) {
|
|
Logging::warn("rolling back");
|
|
Logging::warn($e->getMessage());
|
|
$con->rollback();
|
|
$return_hash['error'] = $e->getMessage();
|
|
}
|
|
return $return_hash;
|
|
}
|
|
|
|
public function reloadMetadataGroupAction()
|
|
{
|
|
// extract all file metadata params from the request.
|
|
// The value is a json encoded hash that has all the information related to this action
|
|
// The key(mdXXX) does not have any meaning as of yet but it could potentially correspond
|
|
// to some unique id.
|
|
$request = $this->getRequest();
|
|
$responses = array();
|
|
$params = $request->getParams();
|
|
$valid_modes = array('delete_dir', 'delete', 'moved', 'modify', 'create');
|
|
foreach ($params as $k => $raw_json) {
|
|
// Valid requests must start with mdXXX where XXX represents at
|
|
// least 1 digit
|
|
if ( !preg_match('/^md\d+$/', $k) ) { continue; }
|
|
$info_json = json_decode($raw_json, $assoc = true);
|
|
|
|
// Log invalid requests
|
|
if ( !array_key_exists('mode', $info_json) ) {
|
|
Logging::info("Received bad request(key=$k), no 'mode' parameter. Bad request is:");
|
|
Logging::info( $info_json );
|
|
array_push( $responses, array(
|
|
'error' => _("Bad request. no 'mode' parameter passed."),
|
|
'key' => $k));
|
|
continue;
|
|
} elseif ( !in_array($info_json['mode'], $valid_modes) ) {
|
|
// A request still has a chance of being invalid even if it
|
|
// exists but it's validated by $valid_modes array
|
|
$mode = $info_json['mode'];
|
|
Logging::info("Received bad request(key=$k). 'mode' parameter was invalid with value: '$mode'. Request:");
|
|
Logging::info( $info_json );
|
|
array_push( $responses, array(
|
|
'error' => _("Bad request. 'mode' parameter is invalid"),
|
|
'key' => $k,
|
|
'mode' => $mode ) );
|
|
continue;
|
|
}
|
|
// Removing 'mode' key from $info_json might not be necessary...
|
|
$mode = $info_json['mode'];
|
|
unset( $info_json['mode'] );
|
|
try {
|
|
$response = $this->dispatchMetadata($info_json, $mode);
|
|
} catch (Exception $e) {
|
|
Logging::warn($e->getMessage());
|
|
Logging::warn(gettype($e));
|
|
}
|
|
// We tack on the 'key' back to every request in case the would like to associate
|
|
// his requests with particular responses
|
|
$response['key'] = $k;
|
|
array_push($responses, $response);
|
|
}
|
|
$this->_helper->json->sendJson($responses);
|
|
}
|
|
|
|
public function listAllFilesAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$dir_id = $request->getParam('dir_id');
|
|
$all = $request->getParam('all');
|
|
|
|
$this->view->files =
|
|
Application_Model_StoredFile::listAllFiles($dir_id, $all);
|
|
}
|
|
|
|
public function listAllWatchedDirsAction()
|
|
{
|
|
$result = array();
|
|
|
|
$arrWatchedDirs = Application_Model_MusicDir::getWatchedDirs();
|
|
$storDir = Application_Model_MusicDir::getStorDir();
|
|
|
|
$result[$storDir->getId()] = $storDir->getDirectory();
|
|
|
|
foreach ($arrWatchedDirs as $watchedDir) {
|
|
$result[$watchedDir->getId()] = $watchedDir->getDirectory();
|
|
}
|
|
|
|
$this->view->dirs = $result;
|
|
}
|
|
|
|
public function addWatchedDirAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$path = base64_decode($request->getParam('path'));
|
|
|
|
$this->view->msg = Application_Model_MusicDir::addWatchedDir($path);
|
|
}
|
|
|
|
public function removeWatchedDirAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$path = base64_decode($request->getParam('path'));
|
|
|
|
$this->view->msg = Application_Model_MusicDir::removeWatchedDir($path);
|
|
}
|
|
|
|
public function setStorageDirAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$path = base64_decode($request->getParam('path'));
|
|
|
|
$this->view->msg = Application_Model_MusicDir::setStorDir($path);
|
|
}
|
|
|
|
public function getStreamSettingAction()
|
|
{
|
|
$info = Application_Model_StreamSetting::getStreamSetting();
|
|
$this->view->msg = $info;
|
|
}
|
|
|
|
public function statusAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$getDiskInfo = $request->getParam('diskinfo') == "true";
|
|
|
|
$status = array(
|
|
"platform"=>Application_Model_Systemstatus::GetPlatformInfo(),
|
|
"airtime_version"=>Application_Model_Preference::GetAirtimeVersion(),
|
|
"services"=>array(
|
|
"pypo"=>Application_Model_Systemstatus::GetPypoStatus(),
|
|
"liquidsoap"=>Application_Model_Systemstatus::GetLiquidsoapStatus(),
|
|
"media_monitor"=>Application_Model_Systemstatus::GetMediaMonitorStatus()
|
|
)
|
|
);
|
|
|
|
if ($getDiskInfo) {
|
|
$status["partitions"] = Application_Model_Systemstatus::GetDiskInfo();
|
|
}
|
|
|
|
$this->view->status = $status;
|
|
}
|
|
|
|
public function registerComponentAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
|
|
$component = $request->getParam('component');
|
|
$remoteAddr = Application_Model_ServiceRegister::GetRemoteIpAddr();
|
|
Logging::info("Registered Component: ".$component."@".$remoteAddr);
|
|
|
|
Application_Model_ServiceRegister::Register($component, $remoteAddr);
|
|
}
|
|
|
|
public function updateLiquidsoapStatusAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
|
|
$msg = $request->getParam('msg_post');
|
|
$stream_id = $request->getParam('stream_id');
|
|
$boot_time = $request->getParam('boot_time');
|
|
|
|
Application_Model_StreamSetting::setLiquidsoapError($stream_id, $msg, $boot_time);
|
|
}
|
|
|
|
public function updateSourceStatusAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
|
|
$sourcename = $request->getParam('sourcename');
|
|
$status = $request->getParam('status');
|
|
|
|
// on source disconnection sent msg to pypo to turn off the switch
|
|
// Added AutoTransition option
|
|
if ($status == "false" && Application_Model_Preference::GetAutoTransition()) {
|
|
$data = array("sourcename"=>$sourcename, "status"=>"off");
|
|
Application_Model_RabbitMq::SendMessageToPypo("switch_source", $data);
|
|
Application_Model_Preference::SetSourceSwitchStatus($sourcename, "off");
|
|
Application_Model_LiveLog::SetEndTime($sourcename == 'scheduled_play'?'S':'L',
|
|
new DateTime("now", new DateTimeZone('UTC')));
|
|
} elseif ($status == "true" && Application_Model_Preference::GetAutoSwitch()) {
|
|
$data = array("sourcename"=>$sourcename, "status"=>"on");
|
|
Application_Model_RabbitMq::SendMessageToPypo("switch_source", $data);
|
|
Application_Model_Preference::SetSourceSwitchStatus($sourcename, "on");
|
|
Application_Model_LiveLog::SetNewLogTime($sourcename == 'scheduled_play'?'S':'L',
|
|
new DateTime("now", new DateTimeZone('UTC')));
|
|
}
|
|
Application_Model_Preference::SetSourceStatus($sourcename, $status);
|
|
}
|
|
|
|
// handles addition/deletion of mount point which watched dirs reside
|
|
public function updateFileSystemMountAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
|
|
$params = $request->getParams();
|
|
$added_list = empty($params['added_dir'])?array():explode(',', $params['added_dir']);
|
|
$removed_list = empty($params['removed_dir'])?array():explode(',', $params['removed_dir']);
|
|
|
|
// get all watched dirs
|
|
$watched_dirs = Application_Model_MusicDir::getWatchedDirs(null, null);
|
|
|
|
foreach ($added_list as $ad) {
|
|
$ad .= '/';
|
|
foreach ($watched_dirs as $dir) {
|
|
$dirPath = $dir->getDirectory();
|
|
|
|
// if mount path itself was watched
|
|
if ($dirPath == $ad) {
|
|
Application_Model_MusicDir::addWatchedDir($dirPath, false);
|
|
} elseif (substr($dirPath, 0, strlen($ad)) === $ad && $dir->getExistsFlag() == false) {
|
|
// if dir contains any dir in removed_list( if watched dir resides on new mounted path )
|
|
Application_Model_MusicDir::addWatchedDir($dirPath, false);
|
|
} elseif (substr($ad, 0, strlen($dirPath)) === $dirPath) {
|
|
// is new mount point within the watched dir?
|
|
// pyinotify doesn't notify anyhing in this case, so we add this mount point as
|
|
// watched dir
|
|
// bypass nested loop check
|
|
Application_Model_MusicDir::addWatchedDir($ad, false, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($removed_list as $rd) {
|
|
$rd .= '/';
|
|
foreach ($watched_dirs as $dir) {
|
|
$dirPath = $dir->getDirectory();
|
|
// if dir contains any dir in removed_list( if watched dir resides on new mounted path )
|
|
if (substr($dirPath, 0, strlen($rd)) === $rd && $dir->getExistsFlag() == true) {
|
|
Application_Model_MusicDir::removeWatchedDir($dirPath, false);
|
|
} elseif (substr($rd, 0, strlen($dirPath)) === $dirPath) {
|
|
// is new mount point within the watched dir?
|
|
// pyinotify doesn't notify anyhing in this case, so we walk through all files within
|
|
// this watched dir in DB and mark them deleted.
|
|
// In case of h) of use cases, due to pyinotify behaviour of noticing mounted dir, we need to
|
|
// compare agaisnt all files in cc_files table
|
|
|
|
$watchDir = Application_Model_MusicDir::getDirByPath($rd);
|
|
// get all the files that is under $dirPath
|
|
$files = Application_Model_StoredFile::listAllFiles(
|
|
$dir->getId(),$all=false);
|
|
foreach ($files as $f) {
|
|
// if the file is from this mount
|
|
if (substr($f->getFilePath(), 0, strlen($rd)) === $rd) {
|
|
$f->delete();
|
|
}
|
|
}
|
|
if ($watchDir) {
|
|
Application_Model_MusicDir::removeWatchedDir($rd, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// handles case where watched dir is missing
|
|
public function handleWatchedDirMissingAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
|
|
$dir = base64_decode($request->getParam('dir'));
|
|
Application_Model_MusicDir::removeWatchedDir($dir, false);
|
|
}
|
|
|
|
/* This action is for use by our dev scripts, that make
|
|
* a change to the database and we want rabbitmq to send
|
|
* out a message to pypo that a potential change has been made. */
|
|
public function rabbitmqDoPushAction()
|
|
{
|
|
Logging::info("Notifying RabbitMQ to send message to pypo");
|
|
|
|
Application_Model_RabbitMq::SendMessageToPypo("reset_liquidsoap_bootstrap", array());
|
|
Application_Model_RabbitMq::PushSchedule();
|
|
}
|
|
|
|
public function getBootstrapInfoAction()
|
|
{
|
|
$live_dj = Application_Model_Preference::GetSourceSwitchStatus('live_dj');
|
|
$master_dj = Application_Model_Preference::GetSourceSwitchStatus('master_dj');
|
|
$scheduled_play = Application_Model_Preference::GetSourceSwitchStatus('scheduled_play');
|
|
|
|
$res = array("live_dj"=>$live_dj, "master_dj"=>$master_dj, "scheduled_play"=>$scheduled_play);
|
|
$this->view->switch_status = $res;
|
|
$this->view->station_name = Application_Model_Preference::GetStationName();
|
|
$this->view->stream_label = Application_Model_Preference::GetStreamLabelFormat();
|
|
$this->view->transition_fade = Application_Model_Preference::GetDefaultTransitionFade();
|
|
}
|
|
|
|
/* This is used but Liquidsoap to check authentication of live streams*/
|
|
public function checkLiveStreamAuthAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
|
|
$username = $request->getParam('username');
|
|
$password = $request->getParam('password');
|
|
$djtype = $request->getParam('djtype');
|
|
|
|
if ($djtype == 'master') {
|
|
//check against master
|
|
if ($username == Application_Model_Preference::GetLiveStreamMasterUsername()
|
|
&& $password == Application_Model_Preference::GetLiveStreamMasterPassword()) {
|
|
$this->view->msg = true;
|
|
} else {
|
|
$this->view->msg = false;
|
|
}
|
|
} elseif ($djtype == "dj") {
|
|
//check against show dj auth
|
|
$showInfo = Application_Model_Show::getCurrentShow();
|
|
// there is current playing show
|
|
if (isset($showInfo[0]['id'])) {
|
|
$current_show_id = $showInfo[0]['id'];
|
|
$CcShow = CcShowQuery::create()->findPK($current_show_id);
|
|
|
|
// get custom pass info from the show
|
|
$custom_user = $CcShow->getDbLiveStreamUser();
|
|
$custom_pass = $CcShow->getDbLiveStreamPass();
|
|
|
|
// get hosts ids
|
|
$show = new Application_Model_Show($current_show_id);
|
|
$hosts_ids = $show->getHostsIds();
|
|
|
|
// check against hosts auth
|
|
if ($CcShow->getDbLiveStreamUsingAirtimeAuth()) {
|
|
foreach ($hosts_ids as $host) {
|
|
$h = new Application_Model_User($host['subjs_id']);
|
|
if ($username == $h->getLogin() && md5($password) == $h->getPassword()) {
|
|
$this->view->msg = true;
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// check against custom auth
|
|
if ($CcShow->getDbLiveStreamUsingCustomAuth()) {
|
|
if ($username == $custom_user && $password == $custom_pass) {
|
|
$this->view->msg = true;
|
|
} else {
|
|
$this->view->msg = false;
|
|
}
|
|
} else {
|
|
$this->view->msg = false;
|
|
}
|
|
} else {
|
|
// no show is currently playing
|
|
$this->view->msg = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* This action is for use by our dev scripts, that make
|
|
* a change to the database and we want rabbitmq to send
|
|
* out a message to pypo that a potential change has been made. */
|
|
public function getFilesWithoutReplayGainAction()
|
|
{
|
|
$dir_id = $this->_getParam('dir_id');
|
|
|
|
//connect to db and get get sql
|
|
$rows = Application_Model_StoredFile::listAllFiles2($dir_id, 100);
|
|
|
|
$this->_helper->json->sendJson($rows);
|
|
}
|
|
|
|
public function getFilesWithoutSilanValueAction()
|
|
{
|
|
//connect to db and get get sql
|
|
$rows = Application_Model_StoredFile::getAllFilesWithoutSilan();
|
|
|
|
$this->_helper->json->sendJson($rows);
|
|
}
|
|
|
|
public function updateReplayGainValueAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$data = json_decode($request->getParam('data'));
|
|
|
|
foreach ($data as $pair) {
|
|
list($id, $gain) = $pair;
|
|
// TODO : move this code into model -- RG
|
|
$file = Application_Model_StoredFile::RecallById($p_id = $id)->getPropelOrm();
|
|
$file->setDbReplayGain($gain);
|
|
$file->save();
|
|
}
|
|
|
|
$this->_helper->json->sendJson(array());
|
|
}
|
|
|
|
public function updateCueValuesBySilanAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$data = json_decode($request->getParam('data'), $assoc = true);
|
|
|
|
foreach ($data as $pair) {
|
|
list($id, $info) = $pair;
|
|
// TODO : move this code into model -- RG
|
|
$file = Application_Model_StoredFile::RecallById($p_id = $id)->getPropelOrm();
|
|
|
|
//What we are doing here is setting a more accurate length that was
|
|
//calculated with silan by actually scanning the entire file. This
|
|
//process takes a really long time, and so we only do it in the background
|
|
//after the file has already been imported -MK
|
|
try {
|
|
$length = $file->getDbLength();
|
|
if (isset($info['length'])) {
|
|
$length = $info['length'];
|
|
//length decimal number in seconds. Need to convert it to format
|
|
//HH:mm:ss to get around silly PHP limitations.
|
|
$length = Application_Common_DateHelper::secondsToPlaylistTime($length);
|
|
$file->setDbLength($length);
|
|
}
|
|
|
|
$cuein = isset($info['cuein']) ? $info['cuein'] : 0;
|
|
$cueout = isset($info['cueout']) ? $info['cueout'] : $length;
|
|
|
|
$file->setDbCuein($cuein);
|
|
$file->setDbCueout($cueout);
|
|
$file->setDbSilanCheck(true);
|
|
$file->save();
|
|
} catch (Exception $e) {
|
|
Logging::info("Failed to update silan values for ".$file->getDbTrackTitle());
|
|
Logging::info("File length analyzed by Silan is: ".$length);
|
|
//set silan_check to true so we don't attempt to re-anaylze again
|
|
$file->setDbSilanCheck(true);
|
|
$file->save();
|
|
}
|
|
}
|
|
|
|
$this->_helper->json->sendJson(array());
|
|
}
|
|
|
|
public function notifyWebstreamDataAction()
|
|
{
|
|
$request = $this->getRequest();
|
|
$data = $request->getParam("data");
|
|
$media_id = intval($request->getParam("media_id"));
|
|
$data_arr = json_decode($data);
|
|
|
|
//$media_id is -1 sometimes when a stream has stopped playing
|
|
if (!is_null($media_id) && $media_id > 0) {
|
|
|
|
if (isset($data_arr->title)) {
|
|
|
|
$data_title = substr($data_arr->title, 0, 1024);
|
|
|
|
$previous_metadata = CcWebstreamMetadataQuery::create()
|
|
->orderByDbStartTime('desc')
|
|
->filterByDbInstanceId($media_id)
|
|
->findOne();
|
|
|
|
$do_insert = true;
|
|
if ($previous_metadata) {
|
|
if ($previous_metadata->getDbLiquidsoapData() == $data_title) {
|
|
Logging::debug("Duplicate found: ". $data_title);
|
|
$do_insert = false;
|
|
}
|
|
}
|
|
|
|
if ($do_insert) {
|
|
|
|
$startDT = new DateTime("now", new DateTimeZone("UTC"));
|
|
|
|
$webstream_metadata = new CcWebstreamMetadata();
|
|
$webstream_metadata->setDbInstanceId($media_id);
|
|
$webstream_metadata->setDbStartTime($startDT);
|
|
$webstream_metadata->setDbLiquidsoapData($data_title);
|
|
$webstream_metadata->save();
|
|
|
|
$historyService = new Application_Service_HistoryService();
|
|
$historyService->insertWebstreamMetadata($media_id, $startDT, $data_arr);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->view->response = $data;
|
|
$this->view->media_id = $media_id;
|
|
}
|
|
|
|
public function getStreamParametersAction() {
|
|
$streams = array("s1", "s2", "s3");
|
|
$stream_params = array();
|
|
foreach ($streams as $s) {
|
|
$stream_params[$s] =
|
|
Application_Model_StreamSetting::getStreamDataNormalized($s);
|
|
}
|
|
$this->view->stream_params = $stream_params;
|
|
}
|
|
|
|
public function pushStreamStatsAction() {
|
|
$request = $this->getRequest();
|
|
$data = json_decode($request->getParam("data"), true);
|
|
|
|
Application_Model_ListenerStat::insertDataPoints($data);
|
|
$this->view->data = $data;
|
|
}
|
|
|
|
public function updateStreamSettingTableAction() {
|
|
$request = $this->getRequest();
|
|
$data = json_decode($request->getParam("data"), true);
|
|
|
|
foreach ($data as $k=>$v) {
|
|
Application_Model_StreamSetting::SetListenerStatError($k, $v);
|
|
}
|
|
}
|
|
}
|