diff --git a/airtime_mvc/application/Bootstrap.php b/airtime_mvc/application/Bootstrap.php index f9a62c9f6..cf4d08731 100644 --- a/airtime_mvc/application/Bootstrap.php +++ b/airtime_mvc/application/Bootstrap.php @@ -71,8 +71,8 @@ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap $view->headScript()->appendFile($baseUrl.'/js/qtip/jquery.qtip-1.0.0.min.js','text/javascript'); //scripts for now playing bar - $view->headScript()->appendFile($baseUrl.'/js/playlist/helperfunctions.js','text/javascript'); - $view->headScript()->appendFile($baseUrl.'/js/playlist/playlist.js','text/javascript'); + $view->headScript()->appendFile($baseUrl.'/js/airtime/dashboard/helperfunctions.js','text/javascript'); + $view->headScript()->appendFile($baseUrl.'/js/airtime/dashboard/playlist.js','text/javascript'); $view->headScript()->appendFile($baseUrl.'/js/airtime/common/common.js','text/javascript'); } diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index 50fe4cc59..a6f0825b7 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -8,9 +8,10 @@ class ApiController extends Zend_Controller_Action /* Initialize action controller here */ $context = $this->_helper->getHelper('contextSwitch'); $context->addActionContext('version', 'json') - ->addActionContext('recorded-shows', 'json') - ->addActionContext('upload-recorded', 'json') - ->initContext(); + ->addActionContext('recorded-shows', 'json') + ->addActionContext('upload-recorded', 'json') + ->addActionContext('reload-metadata', 'json') + ->initContext(); } public function indexAction() @@ -113,8 +114,6 @@ class ApiController extends Zend_Controller_Action $this->view->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(true); - $result = Schedule::GetPlayOrderRange(0, 1); - $date = new DateHelper; $timeNow = $date->getTimestamp(); $result = array("env"=>APPLICATION_ENV, @@ -123,7 +122,7 @@ class ApiController extends Zend_Controller_Action "nextShow"=>Show_DAL::GetNextShows($timeNow, 5), "timezone"=> date("T"), "timezoneOffset"=> date("Z")); - + //echo json_encode($result); header("Content-type: text/javascript"); echo $_GET['callback'].'('.json_encode($result).')'; @@ -316,7 +315,37 @@ class ApiController extends Zend_Controller_Action } } - $this->view->id = $file->getId(); + $this->view->id = $file->getId(); + } + + public function reloadMetadataAction() { + + global $CC_CONFIG; + + $api_key = $this->_getParam('api_key'); + if (!in_array($api_key, $CC_CONFIG["apiKey"])) + { + header('HTTP/1.0 401 Unauthorized'); + print 'You are not allowed to access this resource.'; + exit; + } + + $md = $this->_getParam('md'); + + $file = StoredFile::Recall(null, $md['gunid']); + if (PEAR::isError($file) || is_null($file)) { + $this->view->response = "File not in Airtime's Database"; + return; + } + + $res = $file->replaceDbMetadata($md); + + if (PEAR::isError($res)) { + $this->view->response = "Metadata Change Failed"; + } + else { + $this->view->response = "Success!"; + } } } diff --git a/airtime_mvc/application/controllers/DashboardController.php b/airtime_mvc/application/controllers/DashboardController.php new file mode 100644 index 000000000..55652fc12 --- /dev/null +++ b/airtime_mvc/application/controllers/DashboardController.php @@ -0,0 +1,22 @@ +getValues(); $file->replaceDbMetadata($formdata); + $data = $formdata; + $data['filepath'] = $file->getRealFilePath(); + RabbitMq::SendFileMetaData($data); + $this->_helper->redirector('index'); } } diff --git a/airtime_mvc/application/forms/EditAudioMD.php b/airtime_mvc/application/forms/EditAudioMD.php index 6a002cd03..022745e6f 100644 --- a/airtime_mvc/application/forms/EditAudioMD.php +++ b/airtime_mvc/application/forms/EditAudioMD.php @@ -2,6 +2,24 @@ class Application_Form_EditAudioMD extends Zend_Form { + /* + "title": "track_title",\ + "artist": "artist_name",\ + "album": "album_title",\ + "genre": "genre",\ + "mood": "mood",\ + "tracknumber": "track_number",\ + "bpm": "bpm",\ + "organization": "label",\ + "composer": "composer",\ + "encodedby": "encoded_by",\ + "conductor": "conductor",\ + "date": "year",\ + "website": "info_url",\ + "isrc": "isrc_number",\ + "copyright": "copyright",\ + */ + public function init() { @@ -37,6 +55,13 @@ class Application_Form_EditAudioMD extends Zend_Form 'filters' => array('StringTrim') )); + // Add mood field + $this->addElement('text', 'track_number', array( + 'label' => 'Track:', + 'class' => 'input_text', + 'filters' => array('StringTrim') + )); + // Add genre field $this->addElement('text', 'genre', array( 'label' => 'Genre:', @@ -77,9 +102,30 @@ class Application_Form_EditAudioMD extends Zend_Form 'filters' => array('StringTrim') )); - // Add language field - $this->addElement('text', 'language', array( - 'label' => 'Language:', + // Add mood field + $this->addElement('text', 'bpm', array( + 'label' => 'BPM:', + 'class' => 'input_text', + 'filters' => array('StringTrim') + )); + + // Add mood field + $this->addElement('text', 'copyright', array( + 'label' => 'Copyright:', + 'class' => 'input_text', + 'filters' => array('StringTrim') + )); + + // Add mood field + $this->addElement('text', 'isrc_number', array( + 'label' => 'ISRC Number:', + 'class' => 'input_text', + 'filters' => array('StringTrim') + )); + + // Add mood field + $this->addElement('text', 'info_url', array( + 'label' => 'Website:', 'class' => 'input_text', 'filters' => array('StringTrim') )); diff --git a/airtime_mvc/application/models/Dashboard.php b/airtime_mvc/application/models/Dashboard.php new file mode 100644 index 000000000..0d0c622b9 --- /dev/null +++ b/airtime_mvc/application/models/Dashboard.php @@ -0,0 +1,115 @@ +$showInstance->getName(), + "starts"=>$showInstance->getShowStart(), + "ends"=>$showInstance->getShowEnd()); + } else { + //return the one that started later. + if ($row[0]["starts"] >= $showInstance->getShowStart()){ + return array("name"=>$row[0]["artist_name"]." - ".$row[0]["track_title"], + "starts"=>$row[0]["starts"], + "ends"=>$row[0]["ends"]); + } else { + return array("name"=>$showInstance->getName(), + "starts"=>$showInstance->getShowStart(), + "ends"=>$showInstance->getShowEnd()); + } + } + } + } + + public static function GetCurrentItem($p_timeNow){ + //get previous show and previous item in the schedule table. + //Compare the two and if the last show was recorded and started + //after the last item in the schedule table, then return the show's + //name. Else return the last item from the schedule. + + $showInstance = ShowInstance::GetCurrentShowInstance($p_timeNow); + $row = Schedule::GetCurrentScheduleItem($p_timeNow); + + if (is_null($showInstance)){ + if (count($row) == 0){ + return null; + } else { + //should never reach here. Doesnt make sense to have + //a schedule item not within a show_instance. + } + } else { + if (count($row) == 0){ + //last item is a show instance + return array("name"=>$showInstance->getName(), + "starts"=>$showInstance->getShowStart(), + "ends"=>$showInstance->getShowEnd(), + "media_item_played"=>false, //TODO + "record"=>$showInstance->isRecorded()); //TODO + } else { + return array("name"=>$row[0]["artist_name"]." - ".$row[0]["track_title"], + "starts"=>$row[0]["starts"], + "ends"=>$row[0]["ends"], + "media_item_played"=>$row[0]["media_item_played"], + "record"=>0); + } + } + } + + public static function GetNextItem($p_timeNow){ + //get previous show and previous item in the schedule table. + //Compare the two and if the last show was recorded and started + //after the last item in the schedule table, then return the show's + //name. Else return the last item from the schedule. + + $showInstance = ShowInstance::GetNextShowInstance($p_timeNow); + $row = Schedule::GetNextScheduleItem($p_timeNow); + + if (is_null($showInstance)){ + if (count($row) == 0){ + return null; + } else { + //should never reach here. Doesnt make sense to have + //a schedule item not within a show_instance. + } + } else { + if (count($row) == 0){ + //last item is a show instance + return array("name"=>$showInstance->getName(), + "starts"=>$showInstance->getShowStart(), + "ends"=>$showInstance->getShowEnd()); + } else { + //return the one that starts sooner. + + if ($row[0]["starts"] <= $showInstance->getShowStart()){ + return array("name"=>$row[0]["artist_name"]." - ".$row[0]["track_title"], + "starts"=>$row[0]["starts"], + "ends"=>$row[0]["ends"]); + } else { + return array("name"=>$showInstance->getName(), + "starts"=>$showInstance->getShowStart(), + "ends"=>$showInstance->getShowEnd()); + } + } + } + } + +} diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php index 1672dd4e2..971933f20 100644 --- a/airtime_mvc/application/models/RabbitMq.php +++ b/airtime_mvc/application/models/RabbitMq.php @@ -40,5 +40,27 @@ class RabbitMq } } + public static function SendFileMetaData($md) + { + global $CC_CONFIG; + + $conn = new AMQPConnection($CC_CONFIG["rabbitmq"]["host"], + $CC_CONFIG["rabbitmq"]["port"], + $CC_CONFIG["rabbitmq"]["user"], + $CC_CONFIG["rabbitmq"]["password"]); + $channel = $conn->channel(); + $channel->access_request($CC_CONFIG["rabbitmq"]["vhost"], false, false, true, true); + + $EXCHANGE = 'airtime-media-monitor'; + $channel->exchange_declare($EXCHANGE, 'direct', false, true); + + $data = json_encode($md); + $msg = new AMQPMessage($data, array('content_type' => 'text/plain')); + + $channel->basic_publish($msg, $EXCHANGE); + $channel->close(); + $conn->close(); + } + } diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index d83056522..d6a647d8e 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -365,9 +365,12 @@ class Schedule { $timeNow = $date->getTimestamp(); return array("env"=>APPLICATION_ENV, "schedulerTime"=>gmdate("Y-m-d H:i:s"), - "previous"=>Schedule::GetScheduledItemData($timeNow, -1, $prev, "24 hours"), - "current"=>Schedule::GetScheduledItemData($timeNow, 0), - "next"=>Schedule::GetScheduledItemData($timeNow, 1, $next, "48 hours"), + //"previous"=>Schedule::GetScheduledItemData($timeNow, -1, $prev, "24 hours"), + //"current"=>Schedule::GetScheduledItemData($timeNow, 0), + //"next"=>Schedule::GetScheduledItemData($timeNow, 1, $next, "48 hours"), + "previous"=>Application_Model_Dashboard::GetPreviousItem($timeNow), + "current"=>Application_Model_Dashboard::GetCurrentItem($timeNow), + "next"=>Application_Model_Dashboard::GetNextItem($timeNow), "currentShow"=>Show_DAL::GetCurrentShow($timeNow), "nextShow"=>Show_DAL::GetNextShows($timeNow, 1), "timezone"=> date("T"), @@ -375,6 +378,52 @@ class Schedule { "apiKey"=>$CC_CONFIG['apiKey'][0]); } + public static function GetLastScheduleItem($p_timeNow){ + global $CC_CONFIG, $CC_DBC; + + $sql = "SELECT *" + ." FROM $CC_CONFIG[scheduleTable] st" + ." LEFT JOIN $CC_CONFIG[filesTable] ft" + ." ON st.file_id = ft.id" + ." WHERE st.ends < TIMESTAMP '$p_timeNow'" + ." ORDER BY st.ends DESC" + ." LIMIT 1"; + + $row = $CC_DBC->GetAll($sql); + return $row; + } + + + public static function GetCurrentScheduleItem($p_timeNow){ + global $CC_CONFIG, $CC_DBC; + + $sql = "SELECT *" + ." FROM $CC_CONFIG[scheduleTable] st" + ." LEFT JOIN $CC_CONFIG[filesTable] ft" + ." ON st.file_id = ft.id" + ." WHERE st.starts <= TIMESTAMP '$p_timeNow'" + ." AND st.ends > TIMESTAMP '$p_timeNow'" + ." LIMIT 1"; + + $row = $CC_DBC->GetAll($sql); + return $row; + } + + public static function GetNextScheduleItem($p_timeNow){ + global $CC_CONFIG, $CC_DBC; + + $sql = "SELECT *" + ." FROM $CC_CONFIG[scheduleTable] st" + ." LEFT JOIN $CC_CONFIG[filesTable] ft" + ." ON st.file_id = ft.id" + ." WHERE st.starts > TIMESTAMP '$p_timeNow'" + ." ORDER BY st.starts" + ." LIMIT 1"; + + $row = $CC_DBC->GetAll($sql); + return $row; + } + /** * Builds an SQL Query for accessing scheduled item information from * the database. diff --git a/airtime_mvc/application/models/Shows.php b/airtime_mvc/application/models/Shows.php index 088b8b49f..7b8a8d21e 100644 --- a/airtime_mvc/application/models/Shows.php +++ b/airtime_mvc/application/models/Shows.php @@ -1718,6 +1718,57 @@ class ShowInstance { return ($diff < 0) ? 0 : $diff; } + + public static function GetLastShowInstance($p_timeNow){ + global $CC_CONFIG, $CC_DBC; + + $sql = "SELECT si.id" + ." FROM $CC_CONFIG[showInstances] si" + ." WHERE si.ends < TIMESTAMP '$p_timeNow'" + ." ORDER BY si.ends DESC" + ." LIMIT 1"; + + $id = $CC_DBC->GetOne($sql); + if (is_null($id)){ + return null; + } else { + return new ShowInstance($id); + } + } + + public static function GetCurrentShowInstance($p_timeNow){ + global $CC_CONFIG, $CC_DBC; + + $sql = "SELECT si.id" + ." FROM $CC_CONFIG[showInstances] si" + ." WHERE si.starts <= TIMESTAMP '$p_timeNow'" + ." AND si.ends > TIMESTAMP '$p_timeNow'" + ." LIMIT 1"; + + $id = $CC_DBC->GetOne($sql); + if (is_null($id)){ + return null; + } else { + return new ShowInstance($id); + } + } + + public static function GetNextShowInstance($p_timeNow){ + global $CC_CONFIG, $CC_DBC; + + $sql = "SELECT si.id" + ." FROM $CC_CONFIG[showInstances] si" + ." WHERE si.starts > TIMESTAMP '$p_timeNow'" + ." ORDER BY si.starts" + ." LIMIT 1"; + + $id = $CC_DBC->GetOne($sql); + if (is_null($id)){ + return null; + } else { + return new ShowInstance($id); + } + } } /* Show Data Access Layer */ diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 847e284f6..d5d4faf80 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -543,16 +543,25 @@ class StoredFile { public function replaceDbMetadata($p_values) { global $CC_CONFIG, $CC_DBC; + + $data = array(); foreach ($p_values as $category => $value) { $escapedValue = pg_escape_string($value); $columnName = $category; if (!is_null($columnName)) { - $sql = "UPDATE ".$CC_CONFIG["filesTable"] - ." SET $columnName='$escapedValue'" - ." WHERE gunid = '".$this->gunid."'"; - $CC_DBC->query($sql); + $data[] = "$columnName='$escapedValue'"; } } + + $data = join(",", $data); + $sql = "UPDATE ".$CC_CONFIG["filesTable"] + ." SET $data" + ." WHERE gunid = '".$this->gunid."'"; + $res = $CC_DBC->query($sql); + if (PEAR::isError($res)) { + $CC_DBC->query("ROLLBACK"); + return $res; + } } public function clearMetadata() diff --git a/airtime_mvc/public/js/playlist/helperfunctions.js b/airtime_mvc/public/js/airtime/dashboard/helperfunctions.js similarity index 100% rename from airtime_mvc/public/js/playlist/helperfunctions.js rename to airtime_mvc/public/js/airtime/dashboard/helperfunctions.js diff --git a/airtime_mvc/public/js/playlist/playlist.js b/airtime_mvc/public/js/airtime/dashboard/playlist.js similarity index 72% rename from airtime_mvc/public/js/playlist/playlist.js rename to airtime_mvc/public/js/airtime/dashboard/playlist.js index c6c37c213..1b2177a1e 100644 --- a/airtime_mvc/public/js/playlist/playlist.js +++ b/airtime_mvc/public/js/airtime/dashboard/playlist.js @@ -1,9 +1,9 @@ var estimatedSchedulePosixTime = null; var localRemoteTimeOffset = null; -var previousSongs = new Array(); -var currentSong = new Array(); -var nextSongs = new Array(); +var previousSong = null; +var currentSong = null; +var nextSong = null; var currentShow = new Array(); var nextShow = new Array(); @@ -25,21 +25,8 @@ var nextShowPrepare = true; var apiKey = ""; -function getTrackInfo(song){ - var str = ""; - - if (song.track_title != null) - str += song.track_title; - if (song.artist_name != null) - str += " - " + song.artist_name; - - str += "," - - return str; -} - function secondsTimer(){ - if (localRemoteTimeOffset != null){ + if (localRemoteTimeOffset !== null){ var date = new Date(); estimatedSchedulePosixTime = date.getTime() - localRemoteTimeOffset; updateProgressBarValue(); @@ -50,9 +37,10 @@ function secondsTimer(){ function newSongStart(){ nextSongPrepare = true; - currentSong[0] = nextSongs.shift(); + currentSong = nextSong; + nextSong = null; - if (typeof notifySongStart == "function") + if (typeof notifySongStart == "function") notifySongStart(); } @@ -80,13 +68,13 @@ function updateProgressBarValue(){ $('#progress-show').attr("style", "width:"+showPercentDone+"%"); var songPercentDone = 0; - if (currentSong.length > 0){ - songPercentDone = (estimatedSchedulePosixTime - currentSong[0].songStartPosixTime)/currentSong[0].songLengthMs*100; + if (currentSong !== null){ + songPercentDone = (estimatedSchedulePosixTime - currentSong.songStartPosixTime)/currentSong.songLengthMs*100; if (songPercentDone < 0 || songPercentDone > 100){ songPercentDone = 0; - currentSong = new Array(); + currentSong = null; } else { - if (currentSong[0].media_item_played == "t" && currentShow.length > 0) + if (currentSong.media_item_played == "t" && currentShow.length > 0) $('#on-air-info').attr("class", "on-air-info on"); else $('#on-air-info').attr("class", "on-air-info off"); @@ -99,8 +87,8 @@ function updateProgressBarValue(){ $('#progress-bar').attr("style", "width:"+songPercentDone+"%"); //calculate how much time left to next song if there is any - if (nextSongs.length > 0 && nextSongPrepare){ - var diff = nextSongs[0].songStartPosixTime - estimatedSchedulePosixTime; + if (nextSong !== null && nextSongPrepare){ + var diff = nextSong.songStartPosixTime - estimatedSchedulePosixTime; if (diff < serverUpdateInterval){ //sometimes the diff is negative (-100ms for example). Still looking @@ -133,20 +121,26 @@ function updatePlaybar(){ $('#current').html("Current: Nothing Scheduled"); $('#next').empty(); $('#next-length').empty(); - if (previousSongs.length > 0){ - $('#previous').text(getTrackInfo(previousSongs[previousSongs.length-1])); - $('#prev-length').text(convertToHHMMSSmm(previousSongs[previousSongs.length-1].songLengthMs)); + if (previousSong !== null){ + $('#previous').text(previousSong.name+","); + $('#prev-length').text(convertToHHMMSSmm(previousSong.songLengthMs)); } - if (currentSong.length > 0){ - $('#current').text(getTrackInfo(currentSong[0])); - } else if (currentShow.length > 0){ + if (currentSong !== null){ + if (currentSong.record == "1") + $('#current').html("Recording: "+currentSong.name+","); + else + $('#current').text(currentSong.name+","); + } + /* + else if (currentShow.length > 0){ if (currentShow[0].record == "1"){ $('#current').html("Current: Recording"); } } - if (nextSongs.length > 0){ - $('#next').text(getTrackInfo(nextSongs[0])); - $('#next-length').text(convertToHHMMSSmm(nextSongs[0].songLengthMs)); + * */ + if (nextSong !== null){ + $('#next').text(nextSong.name+","); + $('#next-length').text(convertToHHMMSSmm(nextSong.songLengthMs)); } $('#start').empty(); @@ -154,18 +148,18 @@ function updatePlaybar(){ $('#time-elapsed').empty(); $('#time-remaining').empty(); $('#song-length').empty(); - for (var i=0; i 0){ - currentSong = obj.current; - calcAdditionalData(currentSong); - } nextShow = obj.nextShow; calcAdditionalShowData(obj.currentShow); @@ -238,17 +231,10 @@ function getScheduleFromServer(){ setTimeout(getScheduleFromServer, serverUpdateInterval); } - -function init() { - //begin producer "thread" - getScheduleFromServer(); - - //begin consumer "thread" - secondsTimer(); - +function setupQtip(){ var qtipElem = $('#about-link'); - if (qtipElem.length > 0) + if (qtipElem.length > 0){ qtipElem.qtip({ content: $('#about-txt').html(), show: 'mouseover', @@ -267,6 +253,17 @@ function init() { name: 'light' // Use the default light style } }); + } +} + +function init() { + //begin producer "thread" + getScheduleFromServer(); + + //begin consumer "thread" + secondsTimer(); + + setupQtip(); } $(document).ready(function() { diff --git a/airtime_mvc/public/js/airtime/schedule/schedule.js b/airtime_mvc/public/js/airtime/schedule/schedule.js index 20c0321f8..a821412d9 100644 --- a/airtime_mvc/public/js/airtime/schedule/schedule.js +++ b/airtime_mvc/public/js/airtime/schedule/schedule.js @@ -208,12 +208,42 @@ function getId() { function buildContentDialog(json){ var dialog = $(json.dialog); + + var viewportwidth; + var viewportheight; + // the more standards compliant browsers (mozilla/netscape/opera/IE7) use + // window.innerWidth and window.innerHeight + + if (typeof window.innerWidth != 'undefined') { + viewportwidth = window.innerWidth, viewportheight = window.innerHeight; + } + + // IE6 in standards compliant mode (i.e. with a valid doctype as the first + // line in the document) + + else if (typeof document.documentElement != 'undefined' + && typeof document.documentElement.clientWidth != 'undefined' + && document.documentElement.clientWidth != 0) { + viewportwidth = document.documentElement.clientWidth; + viewportheight = document.documentElement.clientHeight; + } + + // older versions of IE + + else { + viewportwidth = document.getElementsByTagName('body')[0].clientWidth; + viewportheight = document.getElementsByTagName('body')[0].clientHeight; + } + + var height = viewportheight * 2/3; + var width = viewportwidth * 4/5; + dialog.dialog({ autoOpen: false, title: 'Show Contents', - width: 1100, - height: 500, + width: width, + height: height, modal: true, close: closeDialog, buttons: {"Ok": function() { diff --git a/airtime_mvc/public/js/playlist/showlistview.js b/airtime_mvc/public/js/playlist/showlistview.js deleted file mode 100644 index 6e2bb6b57..000000000 --- a/airtime_mvc/public/js/playlist/showlistview.js +++ /dev/null @@ -1,69 +0,0 @@ -function createDataGrid(datagridData){ - - var columnHeaders = [ - { "sTitle": "name" }, - { "sTitle": "date" }, - { "sTitle": "start time" }, - { "sTitle": "end time" } - ]; - - $('#demo').html( '
' ); - $('#nowplayingtable').dataTable( { - "bSort" : false, - "bJQueryUI": true, - "bFilter": false, - "bInfo": false, - "bLengthChange": false, - "aaData": datagridData.rows, - "aoColumns": columnHeaders - } ); - - - var options1 = [ - - {title:"Menu Item 1 - Go TO www.google.com", action:{type:"gourl",url:"http://www.google.com/"}}, - {title:"Menu Item 2 - do nothing"}, - {title:"Menu Item 3 - submenu", type:"sub", src:[{title:"Submenu 1"},{title:"Submenu 2"},{title:"Submenu 3"}, {title:"Submenu 4 - submenu", type:"sub", src:[{title:"SubSubmenu 1"},{title:"SubSubmenu 2"}]}]}, - {title:"Menu Item 4 - Js function", action:{type:"fn",callback:"(function(){ alert('THIS IS THE TEST'); })"}} - ]; - - - var userData = {}; - - var effects = { - show:"default", //type of show effect - orientation: "auto", //type of menu orientation - to top, to bottom, auto (to bottom, if doesn't fit on screen - to top) - xposition:"mouse", // position of menu (left side or right side of trigger element) - yposition:"mouse" - } - - $('#demo').jjmenu('both', options1, userData, effects ); -} - -function initShowListView(){ - - - $.ajax({ url: "/Schedule/get-show-data/format/json", dataType:"text", success:function(data){ - $('#json-string').text(data); - }}); - - - - $.ajax({ url: "/Schedule/get-show-data/format/json", dataType:"json", success:function(data){ - var temp = data.data; - var rows = new Array(); - for (var i=0; i 'Displays usage information.', 'overwrite|o' => 'Overwrite any existing config files.', - 'preserve|p' => 'Keep any existing config files.' + 'preserve|p' => 'Keep any existing config files.', + 'no-db|n' => 'Turn off database install.' ) ); $opts->parse(); @@ -34,6 +35,10 @@ if (isset($opts->h)) { echo $opts->getUsageMessage(); exit; } +$db_install = true; +if (isset($opts->n)){ + $db_install = false; +} $overwrite = false; if (isset($opts->o)) { @@ -72,17 +77,21 @@ require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/conf.php') echo "* Airtime Version: ".AIRTIME_VERSION.PHP_EOL; +if ($db_install) { + //echo PHP_EOL."*** Database Installation ***".PHP_EOL; -AirtimeInstall::CreateDatabaseUser(); +/* AirtimeInstall::CreateDatabaseUser(); -AirtimeInstall::CreateDatabase(); + AirtimeInstall::CreateDatabase(); -AirtimeInstall::DbConnect(true); + AirtimeInstall::DbConnect(true); -AirtimeInstall::InstallPostgresScriptingLanguage(); + AirtimeInstall::InstallPostgresScriptingLanguage(); -AirtimeInstall::CreateDatabaseTables(); + AirtimeInstall::CreateDatabaseTables();*/ + require( 'airtime-db-install.php' ); +} AirtimeInstall::InstallStorageDirectory(); @@ -98,6 +107,9 @@ system("python ".__DIR__."/../python_apps/pypo/install/pypo-install.py"); echo PHP_EOL."*** Recorder Installation ***".PHP_EOL; system("python ".__DIR__."/../python_apps/show-recorder/install/recorder-install.py"); +echo PHP_EOL."*** Media Monitor Installation ***".PHP_EOL; +system("python ".__DIR__."/../python_apps/pytag-fs/install/media-monitor-install.py"); + AirtimeInstall::SetAirtimeVersion(AIRTIME_VERSION); echo "******************************* Install Complete *******************************".PHP_EOL; diff --git a/install/airtime-uninstall.php b/install/airtime-uninstall.php index 3da55df11..065ff6f68 100644 --- a/install/airtime-uninstall.php +++ b/install/airtime-uninstall.php @@ -29,7 +29,10 @@ AirtimeInstall::UninstallPhpCode(); // still be a connection to the database and you wont be able to delete it. //------------------------------------------------------------------------ echo " * Dropping the database '".$CC_CONFIG['dsn']['database']."'...".PHP_EOL; -$command = "su postgres -c \"dropdb {$CC_CONFIG['dsn']['database']}\""; + +// check if DB exists +$command = "echo \"DROP DATABASE IF EXISTS ".$CC_CONFIG['dsn']['database']."\" | sudo -u postgres psql"; + @exec($command, $output, $dbDeleteFailed); //------------------------------------------------------------------------ @@ -66,7 +69,7 @@ if ($dbDeleteFailed) { // Delete the user //------------------------------------------------------------------------ echo " * Deleting database user '{$CC_CONFIG['dsn']['username']}'...".PHP_EOL; -$command = "echo \"DROP USER {$CC_CONFIG['dsn']['username']}\" | su postgres -c psql"; +$command = "echo \"DROP USER IF EXISTS {$CC_CONFIG['dsn']['username']}\" | su postgres -c psql"; @exec($command, $output, $results); if ($results == 0) { echo " * User '{$CC_CONFIG['dsn']['username']}' deleted.".PHP_EOL; @@ -85,6 +88,10 @@ echo PHP_EOL."*** Uninstalling Show Recorder ***".PHP_EOL; $command = "python ".__DIR__."/../python_apps/show-recorder/install/recorder-uninstall.py"; system($command); +echo PHP_EOL."*** Uninstalling Media Monitor ***".PHP_EOL; +$command = "python ".__DIR__."/../python_apps/pytag-fs/install/media-monitor-uninstall.py"; +system($command); + #Disabled as this should be a manual process #AirtimeIni::RemoveIniFiles(); diff --git a/install/include/AirtimeIni.php b/install/include/AirtimeIni.php index 930b6260d..70c059af2 100644 --- a/install/include/AirtimeIni.php +++ b/install/include/AirtimeIni.php @@ -26,13 +26,15 @@ class AirtimeIni const CONF_FILE_PYPO = "/etc/airtime/pypo.cfg"; const CONF_FILE_RECORDER = "/etc/airtime/recorder.cfg"; const CONF_FILE_LIQUIDSOAP = "/etc/airtime/liquidsoap.cfg"; + const CONF_FILE_MEDIAMONITOR = "/etc/airtime/MediaMonitor.cfg"; public static function IniFilesExist() { $configFiles = array(AirtimeIni::CONF_FILE_AIRTIME, AirtimeIni::CONF_FILE_PYPO, AirtimeIni::CONF_FILE_RECORDER, - AirtimeIni::CONF_FILE_LIQUIDSOAP); + AirtimeIni::CONF_FILE_LIQUIDSOAP, + AirtimeIni::CONF_FILE_MEDIAMONITOR); $exist = false; foreach ($configFiles as $conf) { if (file_exists($conf)) { @@ -72,6 +74,10 @@ class AirtimeIni echo "Could not copy liquidsoap.cfg to /etc/airtime/. Exiting."; exit(1); } + if (!copy(__DIR__."/../../python_apps/pytag-fs/MediaMonitor.cfg", AirtimeIni::CONF_FILE_MEDIAMONITOR)){ + echo "Could not copy MediaMonitor.cfg to /etc/airtime/. Exiting."; + exit(1); + } } /** @@ -96,6 +102,10 @@ class AirtimeIni unlink(AirtimeIni::CONF_FILE_LIQUIDSOAP); } + if (file_exists(AirtimeIni::CONF_FILE_MEDIAMONITOR)){ + unlink(AirtimeIni::CONF_FILE_MEDIAMONITOR); + } + if (file_exists("etc/airtime")){ rmdir("/etc/airtime/"); } @@ -171,6 +181,7 @@ class AirtimeIni AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_AIRTIME, 'airtime_dir', AirtimeInstall::CONF_DIR_WWW); AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_PYPO, 'api_key', "'$api_key'"); AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_RECORDER, 'api_key', "'$api_key'"); + AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_MEDIAMONITOR, 'api_key', "'$api_key'"); AirtimeIni::UpdateIniValue(AirtimeInstall::CONF_DIR_WWW.'/build/build.properties', 'project.home', AirtimeInstall::CONF_DIR_WWW); } } diff --git a/install/include/AirtimeInstall.php b/install/include/AirtimeInstall.php index 4395bef81..d240503a7 100644 --- a/install/include/AirtimeInstall.php +++ b/install/include/AirtimeInstall.php @@ -126,7 +126,7 @@ class AirtimeInstall $username = $CC_CONFIG['dsn']['username']; $password = $CC_CONFIG['dsn']['password']; - $command = "echo \"CREATE USER $username ENCRYPTED PASSWORD '$password' LOGIN CREATEDB NOCREATEUSER;\" | su postgres -c psql"; + $command = "echo \"CREATE USER $username ENCRYPTED PASSWORD '$password' LOGIN CREATEDB NOCREATEUSER;\" | su postgres -c psql 2>/dev/null"; @exec($command, $output, $results); if ($results == 0) { @@ -150,7 +150,7 @@ class AirtimeInstall $database = $CC_CONFIG['dsn']['database']; $username = $CC_CONFIG['dsn']['username']; - $command = "echo \"CREATE DATABASE $database OWNER $username\" | su postgres -c psql"; + $command = "echo \"CREATE DATABASE $database OWNER $username\" | su postgres -c psql 2>/dev/null"; @exec($command, $output, $results); if ($results == 0) { diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index f551ebc5f..849824090 100644 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -19,6 +19,7 @@ import json import os from urlparse import urlparse +AIRTIME_VERSION = "1.9.0" def api_client_factory(config): if config["api_client"] == "airtime": @@ -29,7 +30,25 @@ def api_client_factory(config): print 'API Client "'+config["api_client"]+'" not supported. Please check your config file.' print sys.exit() - + +def recursive_urlencode(d): + def recursion(d, base=None): + pairs = [] + + for key, value in d.items(): + if hasattr(value, 'values'): + pairs += recursion(value, key) + else: + new_pair = None + if base: + new_pair = "%s[%s]=%s" % (base, urllib.quote(unicode(key)), urllib.quote(unicode(value))) + else: + new_pair = "%s=%s" % (urllib.quote(unicode(key)), urllib.quote(unicode(value))) + pairs.append(new_pair) + return pairs + + return '&'.join(recursion(d)) + class ApiClientInterface: # Implementation: optional @@ -97,6 +116,9 @@ class ApiClientInterface: def upload_recorded_show(self): pass + + def update_media_metadata(self, md): + pass # Put here whatever tests you want to run to make sure your API is working def test(self): @@ -121,6 +143,8 @@ class AirTimeApiClient(ApiClientInterface): logger.debug("Trying to contact %s", url) url = url.replace("%%api_key%%", self.config["api_key"]) + version = -1 + response = None try: response = urllib.urlopen(url) data = response.read() @@ -129,34 +153,26 @@ class AirTimeApiClient(ApiClientInterface): version = response_json['version'] logger.debug("Airtime Version %s detected", version) except Exception, e: - try: - if e[1] == 401: - if (verbose): - print '#####################################' - print '# YOUR API KEY SEEMS TO BE INVALID:' - print '# ' + self.config["api_key"] - print '#####################################' - return False - except Exception, e: - pass + if e[1] == 401: + if (verbose): + print '#####################################' + print '# YOUR API KEY SEEMS TO BE INVALID:' + print '# ' + self.config["api_key"] + print '#####################################' + return -1 - try: - if e[1] == 404: - if (verbose): - print '#####################################' - print '# Unable to contact the Airtime-API' - print '# ' + url - print '#####################################' - return False - except Exception, e: - pass + if e[1] == 404: + if (verbose): + print '#####################################' + print '# Unable to contact the Airtime-API' + print '# ' + url + print '#####################################' + return -1 - version = 0 logger.error("Unable to detect Airtime Version - %s, Response: %s", e, response) return version - def test(self): logger = logging.getLogger() status, items = self.get_schedule('2010-01-01-00-00-00', '2011-01-01-00-00-00') @@ -175,12 +191,12 @@ class AirTimeApiClient(ApiClientInterface): def is_server_compatible(self, verbose = True): version = self.__get_airtime_version(verbose) - if (version == 0 or version == False): + if (version == -1): if (verbose): print 'Unable to get Airtime version number.' print return False - elif (version[0:4] != "1.9."): + elif (version[0:3] != AIRTIME_VERSION): if (verbose): print 'Airtime version: ' + str(version) print 'pypo not compatible with this version of Airtime.' @@ -198,7 +214,6 @@ class AirTimeApiClient(ApiClientInterface): logger = logging.getLogger() # Construct the URL - #export_url = self.config["base_url"] + self.config["api_base"] + self.config["export_url"] export_url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["export_url"]) logger.info("Fetching schedule from %s", export_url) @@ -220,8 +235,6 @@ class AirTimeApiClient(ApiClientInterface): logger = logging.getLogger() try: - #src = "http://%s:%s/%s/%s" % \ - #(self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["get_media_url"]) src = uri + "/api_key/%%api_key%%" logger.info("try to download from %s to %s", src, dst) src = src.replace("%%api_key%%", self.config["api_key"]) @@ -239,7 +252,6 @@ class AirTimeApiClient(ApiClientInterface): logger = logging.getLogger() playlist = schedule[pkey] schedule_id = playlist["schedule_id"] - #url = self.config["base_url"] + self.config["api_base"] + self.config["update_item_url"] url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_item_url"]) url = url.replace("%%schedule_id%%", str(schedule_id)) @@ -268,7 +280,6 @@ class AirTimeApiClient(ApiClientInterface): response = '' try: schedule_id = data - #url = self.config["base_url"] + self.config["api_base"] + self.config["update_start_playing_url"] url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_start_playing_url"]) url = url.replace("%%media_id%%", str(media_id)) url = url.replace("%%schedule_id%%", str(schedule_id)) @@ -299,7 +310,6 @@ class AirTimeApiClient(ApiClientInterface): response = None try: url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["show_schedule_url"]) - #url = self.config["base_url"] + self.config["api_base"] + self.config["show_schedule_url"] logger.debug(url) url = url.replace("%%api_key%%", self.config["api_key"]) @@ -319,7 +329,6 @@ class AirTimeApiClient(ApiClientInterface): retries = int(self.config["upload_retries"]) retries_wait = int(self.config["upload_wait"]) - #url = self.config["base_url"] + self.config["api_base"] + self.config["upload_file_url"] url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["upload_file_url"]) logger.debug(url) @@ -346,6 +355,26 @@ class AirTimeApiClient(ApiClientInterface): time.sleep(retries_wait) return response + + def update_media_metadata(self, md): + logger = logging.getLogger() + response = None + try: + url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_media_url"]) + logger.debug(url) + url = url.replace("%%api_key%%", self.config["api_key"]) + + data = recursive_urlencode(md) + req = urllib2.Request(url, data) + + response = urllib2.urlopen(req).read() + logger.info("update media %s", response) + response = json.loads(response) + + except Exception, e: + logger.error("Exception: %s", e) + + return response diff --git a/python_apps/pypo/install/pypo-install.py b/python_apps/pypo/install/pypo-install.py index c547081ca..716521d88 100755 --- a/python_apps/pypo/install/pypo-install.py +++ b/python_apps/pypo/install/pypo-install.py @@ -64,6 +64,21 @@ def get_current_script_dir(): #print current_script_dir[0:index] return current_script_dir[0:index] +def is_natty(): + try: + f = open('/etc/lsb-release') + except IOError as e: + #File doesn't exist, so we're not even dealing with Ubuntu + return False + + for line in f: + split = line.split("=") + split[0] = split[0].strip(" \r\n") + split[1] = split[1].strip(" \r\n") + if split[0] == "DISTRIB_CODENAME" and split[1] == "natty": + return True + return False + try: # load config file @@ -93,11 +108,20 @@ try: create_path(config["cache_dir"]) create_path(config["file_dir"]) create_path(config["tmp_dir"]) + + architecture = platform.architecture()[0] + natty = is_natty() - if platform.architecture()[0] == '64bit': + if architecture == '64bit' and natty: + print "Installing 64-bit liquidsoap binary (Natty)" + shutil.copy("%s/../liquidsoap/liquidsoap-amd64-natty"%current_script_dir, "%s/../liquidsoap/liquidsoap"%current_script_dir) + elif architecture == '32bit' and natty: + print "Installing 32-bit liquidsoap binary (Natty)" + shutil.copy("%s/../liquidsoap/liquidsoap-i386-natty"%current_script_dir, "%s/../liquidsoap/liquidsoap"%current_script_dir) + elif architecture == '64bit' and not natty: print "Installing 64-bit liquidsoap binary" shutil.copy("%s/../liquidsoap/liquidsoap-amd64"%current_script_dir, "%s/../liquidsoap/liquidsoap"%current_script_dir) - elif platform.architecture()[0] == '32bit': + elif architecture == '32bit' and not natty: print "Installing 32-bit liquidsoap binary" shutil.copy("%s/../liquidsoap/liquidsoap-i386"%current_script_dir, "%s/../liquidsoap/liquidsoap"%current_script_dir) else: diff --git a/python_apps/pypo/liquidsoap/liquidsoap-amd64-natty b/python_apps/pypo/liquidsoap/liquidsoap-amd64-natty new file mode 100755 index 000000000..7aca06e45 Binary files /dev/null and b/python_apps/pypo/liquidsoap/liquidsoap-amd64-natty differ diff --git a/python_apps/pypo/liquidsoap/liquidsoap-i386-natty b/python_apps/pypo/liquidsoap/liquidsoap-i386-natty new file mode 100755 index 000000000..ff22395fc Binary files /dev/null and b/python_apps/pypo/liquidsoap/liquidsoap-i386-natty differ diff --git a/python_apps/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py index 87b24b8b4..77a1573fe 100755 --- a/python_apps/pypo/pypo-cli.py +++ b/python_apps/pypo/pypo-cli.py @@ -3,29 +3,14 @@ """ Python part of radio playout (pypo) - -The main functions are "fetch" (./pypo_cli.py -f) and "push" (./pypo_cli.py -p) """ - import time -#import calendar -#import traceback from optparse import * import sys import os import signal -#import datetime import logging import logging.config -#import shutil -#import urllib -#import urllib2 -#import pickle -#import telnetlib -#import random -#import string -#import operator -#import inspect from Queue import Queue from pypopush import PypoPush @@ -50,8 +35,6 @@ parser = OptionParser(usage=usage) parser.add_option("-v", "--compat", help="Check compatibility with server API version", default=False, action="store_true", dest="check_compat") parser.add_option("-t", "--test", help="Do a test to make sure everything is working properly.", default=False, action="store_true", dest="test") -parser.add_option("-f", "--fetch-scheduler", help="Fetch the schedule from server. This is a polling process that runs forever.", default=False, action="store_true", dest="fetch_scheduler") -parser.add_option("-p", "--push-scheduler", help="Push the schedule to Liquidsoap. This is a polling process that runs forever.", default=False, action="store_true", dest="push_scheduler") parser.add_option("-b", "--cleanup", help="Cleanup", default=False, action="store_true", dest="cleanup") parser.add_option("-c", "--check", help="Check the cached schedule and exit", default=False, action="store_true", dest="check") @@ -76,8 +59,7 @@ class Global: def selfcheck(self): self.api_client = api_client.api_client_factory(config) - if (not self.api_client.is_server_compatible()): - sys.exit() + return self.api_client.is_server_compatible() def set_export_source(self, export_source): self.export_source = export_source @@ -130,7 +112,8 @@ if __name__ == '__main__': # initialize g = Global() - g.selfcheck() + + while not g.selfcheck(): time.sleep(5000) logger = logging.getLogger() diff --git a/python_apps/pytag-fs/MediaMonitor.cfg b/python_apps/pytag-fs/MediaMonitor.cfg index e4610b720..5f2a79a83 100644 --- a/python_apps/pytag-fs/MediaMonitor.cfg +++ b/python_apps/pytag-fs/MediaMonitor.cfg @@ -7,11 +7,8 @@ base_port = 80 # where the binary files live bin_dir = '/usr/lib/airtime/media-monitor' -# base path to store recordered shows at -base_recorded_files = '/var/tmp/airtime/show-recorder/' - # where the logging files live -log_dir = '/var/log/airtime/show-recorder' +log_dir = '/var/log/airtime/media-monitor' # Value needed to access the API api_key = 'AAA' @@ -22,8 +19,18 @@ api_base = 'api' # URL to get the version number of the server API version_url = 'version/api_key/%%api_key%%' -# URL to get the schedule of shows set to record -show_schedule_url = 'recorded-shows/format/json/api_key/%%api_key%%' +# URL to tell Airtime to update file's meta data +update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%' -# URL to upload the recorded show's file to Airtime -upload_file_url = 'upload-recorded/format/json/api_key/%%api_key%%' +############################################ +# RabbitMQ settings # +############################################ +rabbitmq_host = 'localhost' +rabbitmq_user = 'guest' +rabbitmq_password = 'guest' + +############################################ +# Media-Monitor preferences # +############################################ +check_filesystem_events = 30 #how long to queue up events performed on the files themselves. +check_airtime_events = 30 #how long to queue metadata input from airtime. diff --git a/python_apps/pytag-fs/MediaMonitor.py b/python_apps/pytag-fs/MediaMonitor.py index 2bdf148aa..38423fcc9 100644 --- a/python_apps/pytag-fs/MediaMonitor.py +++ b/python_apps/pytag-fs/MediaMonitor.py @@ -1,6 +1,26 @@ +#!/usr/local/bin/python +import logging +import logging.config +import json +import time +import datetime import os +import sys +import hashlib +import json + +from subprocess import Popen, PIPE, STDOUT + +from configobj import ConfigObj + +import mutagen import pyinotify -from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent +from pyinotify import WatchManager, Notifier, ProcessEvent + +# For RabbitMQ +from kombu.connection import BrokerConnection +from kombu.messaging import Exchange, Queue, Consumer, Producer +from api_clients import api_client # configure logging try: @@ -11,41 +31,183 @@ except Exception, e: # loading config file try: - config = ConfigObj('/etc/airtime/recorder.cfg') + config = ConfigObj('/etc/airtime/MediaMonitor.cfg') except Exception, e: print 'Error loading config file: ', e sys.exit() -# watched events -mask = pyinotify.ALL_EVENTS +""" +list of supported easy tags in mutagen version 1.20 +['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', 'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', 'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', 'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', 'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', 'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', 'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', 'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak', 'organization'] +""" -wm = WatchManager() -wdd = wm.add_watch('/srv/airtime/stor', mask, rec=True) +def checkRabbitMQ(notifier): + try: + notifier.connection.drain_events(timeout=int(config["check_airtime_events"])) + except Exception, e: + logger = logging.getLogger('root') + logger.info("%s", e) -class PTmp(ProcessEvent): - def process_IN_CREATE(self, event): - if event.dir : - global wm - wdd = wm.add_watch(event.pathname, mask, rec=True) - #print wdd.keys() +class AirtimeNotifier(Notifier): + + def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=0, timeout=None): + Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout) + + self.airtime2mutagen = {\ + "track_title": "title",\ + "artist_name": "artist",\ + "album_title": "album",\ + "genre": "genre",\ + "mood": "mood",\ + "track_number": "tracknumber",\ + "bpm": "bpm",\ + "label": "organization",\ + "composer": "composer",\ + "encoded_by": "encodedby",\ + "conductor": "conductor",\ + "year": "date",\ + "info_url": "website",\ + "isrc_number": "isrc",\ + "copyright": "copyright",\ + } - print "%s: %s" % (event.maskname, os.path.join(event.path, event.name)) + schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True) + schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem") + self.connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/") + channel = self.connection.channel() + consumer = Consumer(channel, schedule_queue) + consumer.register_callback(self.handle_message) + consumer.consume() + def handle_message(self, body, message): + # ACK the message to take it off the queue + message.ack() + + logger = logging.getLogger('root') + logger.info("Received md from RabbitMQ: " + body) + + m = json.loads(message.body) + airtime_file = mutagen.File(m['filepath'], easy=True) + del m['filepath'] + for key in m.keys() : + if m[key] != "" : + airtime_file[self.airtime2mutagen[key]] = m[key] + + airtime_file.save() + +class MediaMonitor(ProcessEvent): + + def my_init(self): + """ + Method automatically called from ProcessEvent.__init__(). Additional + keyworded arguments passed to ProcessEvent.__init__() are then + delegated to my_init(). + """ + self.api_client = api_client.api_client_factory(config) + + self.mutagen2airtime = {\ + "title": "track_title",\ + "artist": "artist_name",\ + "album": "album_title",\ + "genre": "genre",\ + "mood": "mood",\ + "tracknumber": "track_number",\ + "bpm": "bpm",\ + "organization": "label",\ + "composer": "composer",\ + "encodedby": "encoded_by",\ + "conductor": "conductor",\ + "date": "year",\ + "website": "info_url",\ + "isrc": "isrc_number",\ + "copyright": "copyright",\ + } + + self.logger = logging.getLogger('root') + + self.temp_files = {} + + def update_airtime(self, event): + self.logger.info("Updating Change to Airtime") + try: + f = open(event.pathname, 'rb') + m = hashlib.md5() + m.update(f.read()) + + md5 = m.hexdigest() + gunid = event.name.split('.')[0] + + md = {'gunid':gunid, 'md5':md5} + + file_info = mutagen.File(event.pathname, easy=True) + attrs = self.mutagen2airtime + for key in file_info.keys() : + if key in attrs : + md[attrs[key]] = file_info[key][0] + + data = {'md': md} + + response = self.api_client.update_media_metadata(data) + + except Exception, e: + self.logger.info("%s", e) + + def process_IN_CREATE(self, event): + if not event.dir : + filename_info = event.name.split(".") + + #file created is a tmp file which will be modified and then moved back to the original filename. + if len(filename_info) > 2 : + self.temp_files[event.pathname] = None + #This is a newly imported file. + else : + pass + + self.logger.info("%s: %s", event.maskname, event.pathname) + + #event.path : /srv/airtime/stor/bd2 + #event.name : bd2aa73b58d9c8abcced989621846e99.mp3 + #event.pathname : /srv/airtime/stor/bd2/bd2aa73b58d9c8abcced989621846e99.mp3 def process_IN_MODIFY(self, event): if not event.dir : - print event.path + filename_info = event.name.split(".") - print "%s: %s" % (event.maskname, os.path.join(event.path, event.name)) + #file modified is not a tmp file. + if len(filename_info) == 2 : + self.update_airtime(event) + + self.logger.info("%s: path: %s name: %s", event.maskname, event.path, event.name) + + def process_IN_MOVED_FROM(self, event): + if event.pathname in self.temp_files : + del self.temp_files[event.pathname] + self.temp_files[event.cookie] = event.pathname + + self.logger.info("%s: %s", event.maskname, event.pathname) + + def process_IN_MOVED_TO(self, event): + if event.cookie in self.temp_files : + del self.temp_files[event.cookie] + self.update_airtime(event) + + self.logger.info("%s: %s", event.maskname, event.pathname) def process_default(self, event): - print "%s: %s" % (event.maskname, os.path.join(event.path, event.name)) + self.logger.info("%s: %s", event.maskname, event.pathname) if __name__ == '__main__': try: - notifier = Notifier(wm, PTmp(), read_freq=2, timeout=1) + # watched events + mask = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO + #mask = pyinotify.ALL_EVENTS + + wm = WatchManager() + wdd = wm.add_watch('/srv/airtime/stor', mask, rec=True, auto_add=True) + + notifier = AirtimeNotifier(wm, MediaMonitor(), read_freq=int(config["check_filesystem_events"]), timeout=1) notifier.coalesce_events() - notifier.loop() + notifier.loop(callback=checkRabbitMQ) except KeyboardInterrupt: notifier.stop() diff --git a/python_apps/pytag-fs/airtime-media-monitor-start b/python_apps/pytag-fs/airtime-media-monitor-start new file mode 100755 index 000000000..084b2b1ad --- /dev/null +++ b/python_apps/pytag-fs/airtime-media-monitor-start @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + +if os.geteuid() != 0: + print "Please run this as root." + sys.exit(1) + +try: + print "Starting daemontool script recorder" + os.system("svc -u /etc/service/recorder") + +except Exception, e: + print "exception:" + str(e) diff --git a/python_apps/pytag-fs/airtime-media-monitor-stop b/python_apps/pytag-fs/airtime-media-monitor-stop new file mode 100755 index 000000000..b51b1ba6a --- /dev/null +++ b/python_apps/pytag-fs/airtime-media-monitor-stop @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess + +if os.geteuid() != 0: + print "Please run this as root." + sys.exit(1) + +try: + print "Stopping daemontool script recorder" + + p1 = subprocess.Popen(["ps", "aux"], stdout=subprocess.PIPE) + p2 = subprocess.Popen(["awk", "/recorder.py/ && !/awk/ {print $2}"], stdin=p1.stdout, stdout=subprocess.PIPE) + recorder_pid = p2.communicate()[0].strip(" \n\r\t") + if (len(recorder_pid) > 0): + os.system("svc -d /etc/service/recorder 1>/dev/null 2>&1") + os.system("svc -d /etc/service/recorder/log 1>/dev/null 2>&1") + os.system("kill -2 %s" % recorder_pid) + print "Success." + else: + print "Not Running." + +except Exception, e: + print "exception:" + str(e) diff --git a/python_apps/pytag-fs/install/media-monitor-daemontools-logger.sh b/python_apps/pytag-fs/install/media-monitor-daemontools-logger.sh new file mode 100755 index 000000000..6819f837d --- /dev/null +++ b/python_apps/pytag-fs/install/media-monitor-daemontools-logger.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec setuidgid pypo multilog t /var/log/airtime/media-monitor/main diff --git a/python_apps/pytag-fs/install/media-monitor-daemontools.sh b/python_apps/pytag-fs/install/media-monitor-daemontools.sh new file mode 100755 index 000000000..d629ec49d --- /dev/null +++ b/python_apps/pytag-fs/install/media-monitor-daemontools.sh @@ -0,0 +1,17 @@ +#!/bin/sh +media_monitor_user="pypo" + +# Location of pypo_cli.py Python script +media_monitor_path="/usr/lib/airtime/media-monitor/" +media_monitor_script="MediaMonitor.py" + +api_client_path="/usr/lib/airtime/pypo/" +cd ${media_monitor_path} + +echo "*** Daemontools: starting daemon" +exec 2>&1 +# Note the -u when calling python! we need it to get unbuffered binary stdout and stderr + +export PYTHONPATH=${api_client_path} +setuidgid ${media_monitor_user} python -u ${media_monitor_path}${media_monitor_script} +# EOF diff --git a/python_apps/pytag-fs/install/media-monitor-install.py b/python_apps/pytag-fs/install/media-monitor-install.py new file mode 100755 index 000000000..8f3fb804e --- /dev/null +++ b/python_apps/pytag-fs/install/media-monitor-install.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import time +import os +import traceback +from optparse import * +import sys +import time +import datetime +import logging +import logging.config +import shutil +import string +import platform +from configobj import ConfigObj +from subprocess import Popen, PIPE, STDOUT + +if os.geteuid() != 0: + print "Please run this as root." + sys.exit(1) + +PATH_INI_FILE = '/etc/airtime/MediaMonitor.cfg' + +def create_path(path): + if not (os.path.exists(path)): + print "Creating directory " + path + os.makedirs(path) + +def create_user(username): + print "Checking for user "+username + p = Popen('id '+username, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) + output = p.stdout.read() + if (output[0:3] != "uid"): + # Make the pypo user + print "Creating user "+username + os.system("adduser --system --quiet --group --shell /bin/bash "+username) + + #set pypo password + p = os.popen('/usr/bin/passwd pypo 1>/dev/null 2>&1', 'w') + p.write('pypo\n') + p.write('pypo\n') + p.close() + else: + print "User already exists." + #add pypo to audio group + os.system("adduser " + username + " audio 1>/dev/null 2>&1") + #add pypo to www-data group + os.system("adduser " + username + " www-data 1>/dev/null 2>&1") + +def copy_dir(src_dir, dest_dir): + if (os.path.exists(dest_dir)) and (dest_dir != "/"): + print "Removing old directory "+dest_dir + shutil.rmtree(dest_dir) + if not (os.path.exists(dest_dir)): + print "Copying directory "+os.path.realpath(src_dir)+" to "+os.path.realpath(dest_dir) + shutil.copytree(src_dir, dest_dir) + +def get_current_script_dir(): + current_script_dir = os.path.realpath(__file__) + index = current_script_dir.rindex('/') + #print current_script_dir[0:index] + return current_script_dir[0:index] + + +try: + # load config file + try: + config = ConfigObj(PATH_INI_FILE) + except Exception, e: + print 'Error loading config file: ', e + sys.exit() + + current_script_dir = get_current_script_dir() + print "Checking and removing any existing media monitor processes" + os.system("python %s/media-monitor-uninstall.py 1>/dev/null 2>&1"% current_script_dir) + time.sleep(5) + + # Create users + create_user("pypo") + + print "Creating log directories" + create_path(config["log_dir"]) + os.system("chmod -R 755 " + config["log_dir"]) + os.system("chown -R pypo:pypo "+config["log_dir"]) + + copy_dir("%s/.."%current_script_dir, config["bin_dir"]) + + print "Setting permissions" + os.system("chmod -R 755 "+config["bin_dir"]) + os.system("chown -R pypo:pypo "+config["bin_dir"]) + + print "Creating symbolic links" + os.system("rm -f /usr/bin/airtime-media-monitor-start") + os.system("ln -s "+config["bin_dir"]+"/airtime-media-monitor-start /usr/bin/") + os.system("rm -f /usr/bin/airtime-media-monitor-stop") + os.system("ln -s "+config["bin_dir"]+"/airtime-media-monitor-stop /usr/bin/") + + print "Installing recorder daemon" + create_path("/etc/service/media-monitor") + create_path("/etc/service/media-monitor/log") + shutil.copy("%s/media-monitor-daemontools.sh"%current_script_dir, "/etc/service/media-monitor/run") + shutil.copy("%s/media-monitor-daemontools-logger.sh"%current_script_dir, "/etc/service/media-monitor/log/run") + os.system("chmod -R 755 /etc/service/media-monitor") + os.system("chown -R pypo:pypo /etc/service/media-monitor") + + print "Waiting for processes to start..." + time.sleep(5) + os.system("python /usr/bin/airtime-media-monitor-start") + time.sleep(2) + + found = True + + p = Popen('svstat /etc/service/media-monitor', shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) + output = p.stdout.read() + if (output.find("unable to open supervise/ok: file does not exist") >= 0): + found = False + print output + + if not found: + print "Media monitor install has completed, but daemontools is not running, please make sure you have it installed and then reboot." +except Exception, e: + print "exception:" + str(e) + sys.exit(1) + + + diff --git a/python_apps/pytag-fs/install/media-monitor-uninstall.py b/python_apps/pytag-fs/install/media-monitor-uninstall.py new file mode 100755 index 000000000..e0a5ca262 --- /dev/null +++ b/python_apps/pytag-fs/install/media-monitor-uninstall.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys +import time +from configobj import ConfigObj + +if os.geteuid() != 0: + print "Please run this as root." + sys.exit(1) + +PATH_INI_FILE = '/etc/airtime/MediaMonitor.cfg' + +def remove_path(path): + os.system("rm -rf " + path) + +def remove_user(username): + os.system("killall -u %s 1>/dev/null 2>&1" % username) + + #allow all process to be completely closed before we attempt to delete user + print "Waiting for processes to close..." + time.sleep(5) + + os.system("deluser --remove-home " + username + " 1>/dev/null 2>&1") + +def get_current_script_dir(): + current_script_dir = os.path.realpath(__file__) + index = current_script_dir.rindex('/') + return current_script_dir[0:index] + +try: + # load config file + try: + config = ConfigObj(PATH_INI_FILE) + except Exception, e: + print 'Error loading config file: ', e + sys.exit() + + os.system("python /usr/bin/airtime-media-monitor-stop") + + print "Removing log directories" + remove_path(config["log_dir"]) + + print "Removing symlinks" + os.system("rm -f /usr/bin/airtime-media-monitor-start") + os.system("rm -f /usr/bin/airtime-media-monitor-stop") + + print "Removing application files" + remove_path(config["bin_dir"]) + + print "Removing daemontool script media-monitor" + remove_path("rm -rf /etc/service/media-monitor") + + remove_user("pypo") + print "Uninstall complete." +except Exception, e: + print "exception:" + str(e) diff --git a/python_apps/show-recorder/install/recorder-daemontools.sh b/python_apps/show-recorder/install/recorder-daemontools.sh index 4190d1112..546f99a3e 100755 --- a/python_apps/show-recorder/install/recorder-daemontools.sh +++ b/python_apps/show-recorder/install/recorder-daemontools.sh @@ -17,7 +17,7 @@ exec 2>&1 export PYTHONPATH=${api_client_path} #su ${recorder_user} -c "python -u ${recorder_path}${recorder_script}" -setuidgid ${recorder_user} ${recorder_path}${recorder_script} +setuidgid ${recorder_user} python -u ${recorder_path}${recorder_script} # EOF diff --git a/python_apps/show-recorder/recorder.py b/python_apps/show-recorder/recorder.py index 7e1f7ef7a..0421086c3 100644 --- a/python_apps/show-recorder/recorder.py +++ b/python_apps/show-recorder/recorder.py @@ -43,7 +43,7 @@ def getDateTimeObj(time): date = timeinfo[0].split("-") time = timeinfo[1].split(":") - return datetime.datetime(int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2])) + return datetime.datetime(int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2])) class ShowRecorder(Thread): @@ -55,6 +55,7 @@ class ShowRecorder(Thread): self.start_time = start_time self.filetype = filetype self.show_instance = show_instance + self.logger = logging.getLogger('root') def record_show(self): @@ -67,11 +68,9 @@ class ShowRecorder(Thread): #-ge:0.1,0.1,0,-1 args = command.split(" ") - print "starting record" - + self.logger.info("starting record") code = call(args) - - print "finishing record, return code %s" % (code) + self.logger.info("finishing record, return code %s", code) return code, filepath @@ -94,55 +93,56 @@ class ShowRecorder(Thread): if code == 0: self.upload_file(filepath) else: - print "problem recording show" + self.logger.info("problem recording show") class Record(): def __init__(self): - self.api_client = api_client.api_client_factory(config) - self.shows_to_record = {} + self.api_client = api_client.api_client_factory(config) + self.shows_to_record = {} + self.logger = logging.getLogger('root') def process_shows(self, shows): self.shows_to_record = {} - + for show in shows: show_starts = getDateTimeObj(show[u'starts']) show_end = getDateTimeObj(show[u'ends']) time_delta = show_end - show_starts - + self.shows_to_record[show[u'starts']] = [time_delta, show[u'instance_id'], show[u'name']] def check_record(self): - + tnow = datetime.datetime.now() sorted_show_keys = sorted(self.shows_to_record.keys()) - + start_time = sorted_show_keys[0] next_show = getDateTimeObj(start_time) - print next_show - print tnow + self.logger.debug("Next show %s", next_show) + self.logger.debug("Now %s", tnow) delta = next_show - tnow min_delta = datetime.timedelta(seconds=60) if delta <= min_delta: - print "sleeping %s seconds until show" % (delta.seconds) + self.logger.debug("sleeping %s seconds until show", delta.seconds) time.sleep(delta.seconds) - + show_length = self.shows_to_record[start_time][0] show_instance = self.shows_to_record[start_time][1] show_name = self.shows_to_record[start_time][2] - - show = ShowRecorder(show_instance, show_length.seconds, show_name, start_time, filetype="mp3", ) + + show = ShowRecorder(show_instance, show_length.seconds, show_name, start_time, filetype="mp3") show.start() - + #remove show from shows to record. del self.shows_to_record[start_time] - + def get_shows(self): @@ -154,7 +154,7 @@ class Record(): if len(shows): self.process_shows(shows) - self.check_record() + self.check_record() if __name__ == '__main__': @@ -165,7 +165,3 @@ if __name__ == '__main__': recorder.get_shows() time.sleep(5) - - - -