diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php index 95c8b3263..df5ef7741 100644 --- a/airtime_mvc/application/configs/constants.php +++ b/airtime_mvc/application/configs/constants.php @@ -4,19 +4,31 @@ define('AIRTIME_VERSION', '1.9.0-devel'); define('AIRTIME_COPYRIGHT_DATE', '2010-2011'); define('AIRTIME_REST_VERSION', '1.1'); -// Metadata Keys -define('UI_MDATA_KEY_TITLE', 'dc:title'); -define('UI_MDATA_KEY_CREATOR', 'dc:creator'); -define('UI_MDATA_KEY_SOURCE', 'dc:source'); -define('UI_MDATA_KEY_DURATION', 'dcterms:extent'); -define('UI_MDATA_KEY_URL', 'ls:url'); -define('UI_MDATA_KEY_FORMAT', 'dc:format'); -define('UI_MDATA_KEY_DESCRIPTION', 'dc:description'); -define('UI_MDATA_KEY_CHANNELS', 'ls:channels'); -define('UI_MDATA_KEY_SAMPLERATE', 'ls:samplerate'); -define('UI_MDATA_KEY_BITRATE', 'ls:bitrate'); -define('UI_MDATA_KEY_ENCODER', 'ls:encoder'); -define('UI_MDATA_KEY_FILENAME', 'ls:filename'); +// Metadata Keys for files +define('MDATA_KEY_FILEPATH', 'filepath'); +define('MDATA_KEY_MD5', 'md5'); +define('MDATA_KEY_TITLE', 'track_title'); +define('MDATA_KEY_CREATOR', 'artist_name'); +define('MDATA_KEY_SOURCE', 'album_title'); +define('MDATA_KEY_DURATION', 'length'); +define('MDATA_KEY_MIME', 'mime'); +define('MDATA_KEY_FTYPE', 'ftype'); +define('MDATA_KEY_URL', 'url'); +define('MDATA_KEY_GENRE', 'genre'); +define('MDATA_KEY_MOOD', 'mood'); +define('MDATA_KEY_LABEL', 'label'); +define('MDATA_KEY_COMPOSER', 'composer'); +define('MDATA_KEY_FORMAT', 'format'); +define('MDATA_KEY_DESCRIPTION', 'description'); +define('MDATA_KEY_SAMPLERATE', 'sample_rate'); +define('MDATA_KEY_BITRATE', 'bit_rate'); +define('MDATA_KEY_ENCODER', 'encoded_by'); +define('MDATA_KEY_ISRC', 'isrc_number'); +define('MDATA_KEY_COPYRIGHT', 'copyright'); +define('MDATA_KEY_YEAR', 'year'); +define('MDATA_KEY_BPM', 'bpm'); +define('MDATA_KEY_TRACKNUMBER', 'track_number'); +define('MDATA_KEY_CONDUCTOR', 'conductor'); define('UI_MDATA_VALUE_FORMAT_FILE', 'File'); define('UI_MDATA_VALUE_FORMAT_STREAM', 'live stream'); diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index 422fa608c..c6d7dfc13 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -10,6 +10,8 @@ class ApiController extends Zend_Controller_Action $context->addActionContext('version', 'json') ->addActionContext('recorded-shows', 'json') ->addActionContext('upload-recorded', 'json') + ->addActionContext('media-monitor-setup', 'json') + ->addActionContext('media-item-status', 'json') ->addActionContext('reload-metadata', 'json') ->initContext(); } @@ -77,7 +79,7 @@ class ApiController extends Zend_Controller_Action if (ctype_alnum($file_id) && strlen($file_id) == 32) { $media = StoredFile::RecallByGunid($file_id); if ($media != null && !PEAR::isError($media)) { - $filepath = $media->getRealFilePath(); + $filepath = $media->getFilePath(); if(!is_file($filepath)) { header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); @@ -293,17 +295,17 @@ class ApiController extends Zend_Controller_Action print 'You are not allowed to access this resource.'; exit; } - + $showCanceled = false; $show_instance = $this->_getParam('show_instance'); - + $upload_dir = ini_get("upload_tmp_dir"); $file = StoredFile::uploadFile($upload_dir); - + $show_name = ""; try { $show_inst = new ShowInstance($show_instance); - + $show_inst->setRecordedFile($file->getId()); $show_name = $show_inst->getName(); $show_genre = $show_inst->getGenre(); @@ -317,12 +319,12 @@ class ApiController extends Zend_Controller_Action //the library), now lets just return. $showCanceled = true; } - + $tmpTitle = !(empty($show_name))?$show_name."-":""; $tmpTitle .= $file->getName(); - + $file->setMetadataValue(UI_MDATA_KEY_TITLE, $tmpTitle); - + if (!$showCanceled && Application_Model_Preference::GetDoSoundCloudUpload()) { for ($i=0; $i<$CC_CONFIG['soundcloud-connection-retries']; $i++) { @@ -353,8 +355,57 @@ class ApiController extends Zend_Controller_Action $this->view->id = $file->getId(); } - public function reloadMetadataAction() { + public function mediaMonitorSetupAction() { + global $CC_CONFIG; + // disable the view and the layout + $this->view->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + + $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; + } + + $plupload_dir = ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload"; + + //need to make sure plupload dir exists so we can watch it. + if(!file_exists($plupload_dir)) { + @mkdir($plupload_dir, 0755); + } + + $this->view->stor = $CC_CONFIG['storageDir']; + $this->view->plupload = $plupload_dir; + } + + public function mediaItemStatusAction() { + 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; + } + + $md5 = $this->_getParam('md5'); + $file = StoredFile::RecallByMd5($md5); + + //New file added to Airtime + if (is_null($file)) { + $this->view->airtime_status = 0; + } + else { + $this->view->airtime_status = 1; + } + + } + + public function reloadMetadataAction() { global $CC_CONFIG; $api_key = $this->_getParam('api_key'); @@ -366,22 +417,62 @@ class ApiController extends Zend_Controller_Action } $md = $this->_getParam('md'); - $filepath = $md['filepath']; - $filepath = str_replace("\\", "", $filepath); - $file = StoredFile::Recall(null, null, null, $filepath); - if (PEAR::isError($file) || is_null($file)) { - $this->view->response = "File not in Airtime's Database"; - return; + $mode = $this->_getParam('mode'); + + if ($mode == "create") { + $md5 = $md['MDATA_KEY_MD5']; + $file = StoredFile::RecallByMd5($md5); + + if (is_null($file)) { + $file = StoredFile::Insert($md); + } + else { + $this->view->error = "File already exists in Airtime."; + return; + } + } + else if ($mode == "modify") { + $filepath = $md['MDATA_KEY_FILEPATH']; + $filepath = str_replace("\\", "", $filepath); + $file = StoredFile::RecallByFilepath($filepath); + + //File is not in database anymore. + if (is_null($file)) { + $this->view->error = "File does not exist in Airtime."; + return; + } + //Updating a metadata change. + else { + $file->setMetadata($md); + } + } + else if ($mode == "moved") { + $md5 = $md['MDATA_KEY_MD5']; + $file = StoredFile::RecallByMd5($md5); + + if (is_null($file)) { + $this->view->error = "File doesn't exist in Airtime."; + return; + } + else { + $file->setMetadata($md); + } + } + else if ($mode == "delete") { + $filepath = $md['MDATA_KEY_FILEPATH']; + $filepath = str_replace("\\", "", $filepath); + $file = StoredFile::RecallByFilepath($filepath); + + if (is_null($file)) { + $this->view->error = "File doesn't exist in Airtime."; + return; + } + else { + $file->delete(); + } } - $res = $file->replaceDbMetadata($md); - - if (PEAR::isError($res)) { - $this->view->response = "Metadata Change Failed"; - } - else { - $this->view->response = "Success!"; - } + $this->view->id = $file->getId(); } } diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 180402d7c..cc45408bf 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -78,7 +78,7 @@ class LibraryController extends Zend_Controller_Action $file_id = $this->_getParam('id', null); $file = StoredFile::Recall($file_id); - $url = $file->getFileURL().'/api_key/'.$CC_CONFIG["apiKey"][0].'/download/true'; + $url = $file->getFileUrl().'/api_key/'.$CC_CONFIG["apiKey"][0].'/download/true'; $menu[] = array('action' => array('type' => 'gourl', 'url' => $url), 'title' => 'Download'); @@ -162,18 +162,18 @@ class LibraryController extends Zend_Controller_Action if ($form->isValid($request->getPost())) { $formdata = $form->getValues(); - $file->replaceDbMetadata($formdata); + $file->setDbColMetadata($formdata); $data = $formdata; - $data['filepath'] = $file->getRealFilePath(); - //wait for 1.9.0 release - //RabbitMq::SendFileMetaData($data); + $data['filepath'] = $file->getFilePath(); + + RabbitMq::SendFileMetaData($data); $this->_helper->redirector('index'); } } - $form->populate($file->md); + $form->populate($file->getDbColMetadata()); $this->view->form = $form; } diff --git a/airtime_mvc/application/controllers/PluploadController.php b/airtime_mvc/application/controllers/PluploadController.php index 56c07bc23..d8fc10c08 100644 --- a/airtime_mvc/application/controllers/PluploadController.php +++ b/airtime_mvc/application/controllers/PluploadController.php @@ -25,9 +25,13 @@ class PluploadController extends Zend_Controller_Action public function uploadAction() { $upload_dir = ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload"; - $file = StoredFile::uploadFile($upload_dir); + $res = StoredFile::uploadFile($upload_dir); - die('{"jsonrpc" : "2.0", "id" : '.$file->getId().' }'); + if (isset($res)) { + die('{"jsonrpc" : "2.0", "id" : '.$file->getMessage().' }'); + } + + die('{"jsonrpc" : "2.0"}'); } } diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php index 170395f76..ee70ae6c5 100644 --- a/airtime_mvc/application/models/RabbitMq.php +++ b/airtime_mvc/application/models/RabbitMq.php @@ -40,9 +40,6 @@ class RabbitMq } } -/* -* wait for 1.9.0 release - public static function SendFileMetaData($md) { global $CC_CONFIG; @@ -64,7 +61,5 @@ class RabbitMq $channel->close(); $conn->close(); } -*/ - } diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 84556ce78..3e27f4393 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -1,329 +1,8 @@ "ftype", - "dc:format" => "format", - "ls:bitrate" => "bit_rate", - "ls:samplerate" => "sample_rate", - "dcterms:extent" => "length", - "dc:title" => "track_title", - "dc:description" => "comments", - "dc:type" => "genre", - "dc:creator" => "artist_name", - "dc:source" => "album_title", - "ls:channels" => "channels", - "ls:filename" => "name", - "ls:year" => "year", - "ls:url" => "url", - "ls:track_num" => "track_number", - "ls:mood" => "mood", - "ls:bpm" => "bpm", - "ls:disc_num" => "disc_number", - "ls:rating" => "rating", - "ls:encoded_by" => "encoded_by", - "dc:publisher" => "label", - "ls:composer" => "composer", - "ls:encoder" => "encoder", - "ls:crc" => "checksum", - "ls:lyrics" => "lyrics", - "ls:orchestra" => "orchestra", - "ls:conductor" => "conductor", - "ls:lyricist" => "lyricist", - "ls:originallyricist" => "original_lyricist", - "ls:radiostationname" => "radio_station_name", - "ls:audiofileinfourl" => "info_url", - "ls:artisturl" => "artist_url", - "ls:audiosourceurl" => "audio_source_url", - "ls:radiostationurl" => "radio_station_url", - "ls:buycdurl" => "buy_this_url", - "ls:isrcnumber" => "isrc_number", - "ls:catalognumber" => "catalog_number", - "ls:originalartist" => "original_artist", - "dc:rights" => "copyright", - "dcterms:temporal" => "report_datetime", - "dcterms:spatial" => "report_location", - "dcterms:entity" => "report_organization", - "dc:subject" => "subject", - "dc:contributor" => "contributor", - "dc:language" => "language"); - - public static function GetMapMetadataXmlToDb() { - return Metadata::$MAP_METADATA_XML_TO_DB; - } - - /** - * Track numbers in metadata tags can come in many formats: - * "1 of 20", "1/20", "20/1". This function parses the track - * number and gets the real number so that we can sort by it - * in the database. - * - * @param string $p_trackNumber - * @return int - */ - public static function ParseTrackNumber($p_trackNumber) - { - $num = trim($p_trackNumber); - if (!is_numeric($num)) { - $matches = preg_match("/\s*([0-9]+)([^0-9]*)([0-9]*)\s*/", $num, $results); - $trackNum = 0; - foreach ($results as $result) { - if (is_numeric($result)) { - if ($trackNum == 0) { - $trackNum = $result; - } elseif ($result < $trackNum) { - $trackNum = $result; - } - } - } - } else { - $trackNum = $num; - } - return $trackNum; - } - - - /** - * Add data to the array $p_mdata. - * - * Converts the given string ($val) into UTF-8. - * - * @param array $p_mdata - * The array to add the metadata to. - * @param string $p_key - * Metadata key. - * @param string $p_val - * Metadata value. - * @param string $p_inputEncoding - * Encoding type of the input value. - */ - public static function AddToArray(&$p_mdata, $p_key, $p_val, $p_inputEncoding='iso-8859-1') - { - if (!is_null($p_val)) { - $data = $p_val; - $outputEncoding = 'UTF-8'; - //if (function_exists('iconv') && ($p_inputEncoding != $outputEncoding) ) { - if (function_exists('iconv') && is_string($p_val)) { - $newData = @iconv($p_inputEncoding, $outputEncoding, $data); - if ($newData === FALSE) { - echo "Warning: convert $key data to unicode failed\n"; - } elseif ($newData != $data) { - echo "Converted string: '$data' (".gettype($data).") -> '$newData' (".gettype($newData).").\n"; - $data = $newData; - } - } - $p_mdata[$p_key] = trim($data); - } - } - - - /** - * Return an array with the given audio file's ID3 tags. The keys in the - * array can be: - *
-     * 		dc:format ("mime type")
-     * 		dcterms:extent ("duration")
-     * 		dc:title
-     * 		dc:creator ("artist")
-     * 		dc:source ("album")
-     *      dc:type ("genre")
-     * 		ls:bitrate
-     * 		ls:encoded_by
-     * 		ls:track_num
-     * 		ls:channels
-     * 		ls:year
-     * 		ls:filename
-     * 
- * - * @param string $p_filename - * @param boolean $p_testonly - * For diagnostic and debugging purposes - setting this to TRUE - * will print out the values found in the file and the ones assigned - * to the return array. - * @return array|PEAR_Error - */ - public static function LoadFromFile($p_filename, $p_testonly = false) - { - $getID3 = new getID3(); - $infoFromFile = $getID3->analyze($p_filename); - if (PEAR::isError($infoFromFile)) { - return $infoFromFile; - } - if (isset($infoFromFile['error'])) { - return new PEAR_Error(array_pop($infoFromFile['error'])); - } - if (!$infoFromFile['bitrate']) { - return new PEAR_Error("File given is not an audio file."); - } - - if ($p_testonly) { - print_r($infoFromFile); - } - $titleKey = 'dc:title'; - $flds = array( - 'dc:format' => array( - array('path'=>"['mime_type']", 'ignoreEnc'=>TRUE), - ), - 'ls:bitrate' => array( - array('path'=>"['bitrate']", 'ignoreEnc'=>TRUE), - array('path'=>"['audio']['bitrate']", 'ignoreEnc'=>TRUE), - ), - 'ls:samplerate' => array( - array('path'=>"['audio']['sample_rate']", 'ignoreEnc'=>TRUE), - ), - 'ls:encoder' => array( - array('path'=>"['audio']['codec']", 'ignoreEnc'=>TRUE), - ), - 'dcterms:extent'=> array( - array('path'=>"['playtime_seconds']", 'ignoreEnc'=>TRUE), - ), - 'ls:composer'=> array( - array('path'=>"['id3v2']['comments']['composer']", 'dataPath'=>"[0]", 'ignoreEnc'=>TRUE), - array('path'=>"['id3v2']['TCOM'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['id3v2']['composer']", 'dataPath'=>"[0]", 'ignoreEnc'=>TRUE), - array('path'=>"['ogg']['comments']['composer']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['composer']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'dc:description'=> array( - array('path'=>"['id3v1']['comments']['comment']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['comments']['comments']", 'dataPath'=>"[0]", 'ignoreEnc'=>TRUE), - array('path'=>"['id3v2']['COMM'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['id3v2']['comments']", 'dataPath'=>"[0]", 'ignoreEnc'=>TRUE), - array('path'=>"['ogg']['comments']['comment']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['comment']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'dc:type'=> array( - array('path'=>"['id3v1']", 'dataPath'=>"['genre']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['comments']['content_type']", 'dataPath'=>"[0]", 'ignoreEnc'=>TRUE), - array('path'=>"['id3v2']['TCON'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['ogg']['comments']['genre']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['genre']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'dc:title' => array( - array('path'=>"['id3v2']['comments']['title']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TIT2'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TT2'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v1']", 'dataPath'=>"['title']", 'encPath'=>"['encoding']"), - array('path'=>"['ogg']['comments']['title']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['title']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'dc:creator' => array( - array('path'=>"['id3v2']['comments']['artist']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TPE1'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TP1'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v1']", 'dataPath'=>"['artist']", 'encPath'=>"['encoding']"), - array('path'=>"['ogg']['comments']['artist']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['artist']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'dc:source' => array( - array('path'=>"['id3v2']['comments']['album']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TALB'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TAL'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['ogg']['comments']['album']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['album']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'ls:encoded_by' => array( - array('path'=>"['id3v2']['TENC'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TEN'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['ogg']['comments']['encoded-by']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['encoded-by']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'ls:track_num' => array( - array('path'=>"['id3v2']['TRCK'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['id3v2']['TRK'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - array('path'=>"['ogg']['comments']['tracknumber']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['tracknumber']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - // 'ls:genre' => array( - // array('path'=>"['id3v1']", 'dataPath'=>"['genre']", 'encPath'=>"['encoding']"), - // array('path'=>"['id3v2']['TCON'][0]", 'dataPath'=>"['data']", 'encPath'=>"['encoding']"), - // array('path'=>"['id3v2']['comments']['content_type']", 'dataPath'=>"[0]", 'ignoreEnc'=>TRUE), - // array('path'=>"['ogg']['comments']['genre']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - // array('path'=>"['tags']['vorbiscomment']['genre']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - // ), - 'ls:channels' => array( - array('path'=>"['audio']['channels']", 'ignoreEnc'=>TRUE), - ), - 'ls:year' => array( - array('path'=>"['comments']['date']"), - array('path'=>"['ogg']['comments']['date']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - array('path'=>"['tags']['vorbiscomment']['date']", 'dataPath'=>"[0]", 'encPath'=>"['encoding']"), - ), - 'ls:filename' => array( - array('path'=>"['filename']"), - ), - ); - $mdata = array(); - if (isset($infoFromFile['audio'])) { - $mdata['audio'] = $infoFromFile['audio']; - } - if (isset($infoFromFile['playtime_seconds'])) { - $mdata['playtime_seconds'] = $infoFromFile['playtime_seconds']; - } - - $titleHaveSet = FALSE; - foreach ($flds as $key => $getid3keys) { - foreach ($getid3keys as $getid3key) { - $path = $getid3key["path"]; - $ignoreEnc = isset($getid3key["ignoreEnc"])? - $getid3key["ignoreEnc"]:FALSE; - $dataPath = isset($getid3key["dataPath"])?$getid3key["dataPath"]:""; - $encPath = isset($getid3key["encPath"])?$getid3key["encPath"]:""; - $enc = "UTF-8"; - - $tagElement = "\$infoFromFile$path$dataPath"; - eval("\$tagExists = isset($tagElement);"); - if ($tagExists) { - //echo "ignore encoding: ".($ignoreEnc?"yes":"no")."\n"; - //echo "tag exists\n"; - //echo "encode path: $encPath\n"; - eval("\$data = $tagElement;"); - if (!$ignoreEnc && $encPath != "") { - $encodedElement = "\$infoFromFile$path$encPath"; - eval("\$encodedElementExists = isset($encodedElement);"); - if ($encodedElementExists) { - eval("\$enc = $encodedElement;"); - } - } - - // Special case handling for track number - if ($key == "ls:track_num") { - $data = Metadata::ParseTrackNumber($data); - } - Metadata::AddToArray($mdata, $key, $data, $enc); - if ($key == $titleKey) { - $titleHaveSet = TRUE; - } - break; - } - } - } - if ($p_testonly) { - var_dump($mdata); - } - - if (!$titleHaveSet || trim($mdata[$titleKey]) == '') { - Metadata::AddToArray($mdata, $titleKey, basename($p_filename)); - } - return $mdata; - } -} - /** * StoredFile class * - * Airtime file storage support class.
- * Represents one virtual file in storage. Virtual file has up to two parts: - * - * * @package Airtime * @subpackage StorageServer * @copyright 2010 Sourcefabric O.P.S. @@ -332,604 +11,185 @@ class Metadata { */ class StoredFile { - // *** Variables stored in the database *** + /** + * @holds propel database object + */ + private $_file; /** - * @var int + * array of db metadata -> propel */ - private $id; + private $_dbMD = array ( + "track_title" => "DbTrackTitle", + "artist_name" => "DbArtistName", + "album_title" => "DbAlbumTitle", + "genre" => "DbGenre", + "mood" => "DbMood", + "track_number" => "DbTrackNumber", + "bpm" => "DbBpm", + "label" => "DbLabel", + "composer" => "DbComposer", + "encoded_by" => "DbEncodedBy", + "conductor" => "DbConductor", + "year" => "DbYear", + "info_url" => "DbInfoUrl", + "isrc_number" => "DbIsrcNumber", + "copyright" => "DbCopyright", + "length" => "DbLength", + "bit_rate" => "DbBitRate", + "sample_rate" => "DbSampleRate", + "mime" => "DbMime", + "filepath" => "DbFilepath", + "md5" => "DbMd5", + "ftype" => "DbFtype" + ); - /** - * Unique ID for the file. This is stored in HEX format. It is - * converted to a bigint whenever it is used in a database call. - * - * @var string - */ - public $gunid; - - /** - * The unique ID of the file as it is stored in the database. - * This is for debugging purposes and may not always exist in this - * class. - * - * @var string - */ - //private $gunidBigint; - - /** - * @var string - */ - private $name; - - /** - * @var string - */ - private $mime; - - /** - * Can be 'audioclip'...others might be coming, like webstream. - * - * @var string - */ - private $ftype; - - /** - * Can be 'ready', 'edited', 'incomplete'. - * - * @var string - */ - private $state; - - /** - * @var int - */ - private $currentlyaccessing; - - /** - * @var int - */ - private $editedby; - - /** - * @var timestamp - */ - private $mtime; - - /** - * @var string - */ - private $md5; - - /** - * @var string - */ - private $filepath; - - - // *** Variables NOT stored in the database *** - - /** - * Directory where the file is located. - * - * @var string - */ - private $resDir; - - /** - * @var boolean - */ - private $exists; - - /** - * @var MetaData - */ - public $md; - - /* ========================================================== constructor */ - /** - * Constructor, but shouldn't be externally called - * - * @param string $p_gunid - * globally unique id of file - */ - public function __construct($p_gunid=NULL) + public function __construct() { - $this->gunid = $p_gunid; - if (empty($this->gunid)) { - $this->gunid = StoredFile::generateGunid(); + + } + + public function getId() + { + return $this->_file->getDbId(); + } + + public function getGunId() { + return $this->_file->getDbGunid(); + } + + public function getFormat() + { + return $this->_file->getDbFtype(); + } + + public function setFormat($p_format) + { + $this->_file->setDbFtype($p_format); + } + + /** + * Set multiple metadata values using defined metadata constants. + * + * @param array $p_md + * example: $p_md['MDATA_KEY_URL'] = 'http://www.fake.com' + */ + public function setMetadata($p_md=null) + { + if (is_null($p_md)) { + $this->setDbColMetadata(); } else { - $this->loadMetadata(); - $this->exists = is_file($this->filepath) && is_readable($this->filepath); + $dbMd = array(); + foreach ($p_md as $mdConst => $mdValue) { + $dbMd[constant($mdConst)] = $mdValue; + } + $this->setDbColMetadata($dbMd); } } /** - * For testing only, do not use. - */ - public function __setGunid($p_guid) { - $this->gunid = $p_guid; - } - - /** - * Convert XML name to database column name. Used for backwards compatibility - * with old code. + * Set multiple metadata values using database columns as indexes. * - * @param string $p_category - * @return string|null + * @param array $p_md + * example: $p_md['url'] = 'http://www.fake.com' */ - public static function xmlCategoryToDbColumn($p_category) + public function setDbColMetadata($p_md=null) { - $map = Metadata::GetMapMetadataXmlToDb(); - if (array_key_exists($p_category, $map)) { - return $map[$p_category]; + if (is_null($p_md)) { + foreach ($this->_dbMD as $dbColumn => $propelColumn) { + $method = "set$propelColumn"; + $this->_file->$method(null); + } } - return null; + else { + foreach ($p_md as $dbColumn => $mdValue) { + //don't blank out name, defaults to original filename on first insertion to database. + if($p_category == "track_title" && (is_null($p_value) || $p_value == "")) { + continue; + } + $propelColumn = $this->_dbMD[$dbColumn]; + $method = "set$propelColumn"; + $this->_file->$method($mdValue); + } + } + + $this->_file->save(); } - /** - * Convert database column name to XML name. + * Set metadata element value * - * @param string $p_dbColumn - * @return string|null + * @param string $category + * Metadata element by metadata constant + * @param string $value + * value to store, if NULL then delete record */ - public static function dbColumnToXmlCatagory($p_dbColumn) + public function setMetadataValue($p_category, $p_value) { - $str = array_search($p_dbColumn, Metadata::GetMapMetadataXmlToDb()); - // make return value consistent with xmlCategoryToDbColumn() - if ($str === FALSE) { - $str = null; - } - return $str; + $this->setDbColMetadataValue(constant($p_category), $p_value); } - /** - * GUNID needs to be set before you call this function. + * Set metadata element value * + * @param string $category + * Metadata element by db column + * @param string $value + * value to store, if NULL then delete record */ - public function loadMetadata() + public function setDbColMetadataValue($p_category, $p_value) { - global $CC_CONFIG, $CC_DBC; - $escapedValue = pg_escape_string($this->gunid); - $sql = "SELECT * FROM ".$CC_CONFIG["filesTable"] - ." WHERE gunid='$escapedValue'"; - - $this->md = $CC_DBC->getRow($sql); - - if (PEAR::isError($this->md)) { - $error = $this->md; - $this->md = null; - return $error; - } - $this->filepath = $this->md["filepath"]; - if (is_null($this->md)) { - $this->md = array(); + //don't blank out name, defaults to original filename on first insertion to database. + if($p_category == "track_title" && (is_null($p_value) || $p_value == "")) { return; } - $compatibilityData = array(); - foreach ($this->md as $key => $value) { - if ($xmlName = StoredFile::dbColumnToXmlCatagory($key)) { - $compatibilityData[$xmlName] = $value; - } - } - - $this->md = array_merge($this->md, $compatibilityData); - //$this->md = $compatibilityData; - } - - public function setFormat($p_value) - { - $this->md["format"] = $p_value; - } - - public function replaceMetadata($p_values) - { - global $CC_CONFIG, $CC_DBC; - foreach ($p_values as $category => $value) { - $escapedValue = pg_escape_string($value); - $columnName = StoredFile::xmlCategoryToDbColumn($category); - if (!is_null($columnName)) { - $sql = "UPDATE ".$CC_CONFIG["filesTable"] - ." SET $columnName='$escapedValue'" - ." WHERE gunid = '".$this->gunid."'"; - $CC_DBC->query($sql); - } - } - $this->loadMetadata(); - } - - public function replaceDbMetadata($p_values) - { - global $CC_CONFIG, $CC_DBC; - - $data = array(); - foreach ($p_values as $category => $value) { - if (isset($value) && ($value != '')) { - $escapedValue = pg_escape_string($value); - $columnName = $category; - if (!is_null($columnName)) { - $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() - { - $metadataColumns = array("format", "bit_rate", "sample_rate", "length", - "track_title", "comments", "genre", "artist_name", "channels", "name", - "year", "url", "track_number"); - foreach ($metadataColumns as $columnName) { - if (!is_null($columnName)) { - $sql = "UPDATE ".$CC_CONFIG["filesTable"] - ." SET $columnName=''" - ." WHERE gunid = '".$this->gunid."'"; - $CC_DBC->query($sql); - } - } - } - - - /** - * Create instance of StoredFile object and insert new file - * - * @param array $p_values - * "filepath" - required, local path to media file (where it is before import) - * "id" - optional, local object id, will be generated if not given - * "gunid" - optional, unique id, for insert file with gunid, will be generated if not given - * "filename" - optional, will use "filepath" if not given - * "metadata" - optional, array of extra metadata, will be automatically calculated if not given. - * "mime" - optional, MIME type, highly recommended to pass in, will be automatically calculated if not given. - * "md5" - optional, MD5 sum, highly recommended to pass in, will be automatically calculated if not given. - * - * @param boolean $p_copyMedia - * copy the media file if true, make symlink if false - * - * @return StoredFile|NULL|PEAR_Error - */ - public static function Insert($p_values, $p_copyMedia=TRUE) - { - global $CC_CONFIG, $CC_DBC; - - if (!isset($p_values["filepath"])) { - return new PEAR_Error("StoredFile::Insert: filepath not set."); - } - if (!file_exists($p_values['filepath'])) { - return PEAR::raiseError("StoredFile::Insert: ". - "media file not found ({$p_values['filepath']})"); - } - - $gunid = isset($p_values['gunid'])?$p_values['gunid']:NULL; - - // Create the StoredFile object - $storedFile = new StoredFile($gunid); - - // Get metadata - if (isset($p_values["metadata"])) { - $metadata = $p_values['metadata']; - } else { - $metadata = Metadata::LoadFromFile($p_values["filepath"]); - } - - $storedFile->name = isset($p_values['filename']) ? $p_values['filename'] : $p_values["filepath"]; - $storedFile->id = isset($p_values['id']) && is_integer($p_values['id'])?(int)$p_values['id']:null; - // NOTE: POSTGRES-SPECIFIC KEYWORD "DEFAULT" BEING USED, WOULD BE "NULL" IN MYSQL - $sqlId = !is_null($storedFile->id)?"'".$storedFile->id."'":'DEFAULT'; - $storedFile->ftype = isset($p_values['filetype']) ? strtolower($p_values['filetype']) : "audioclip"; - $storedFile->mime = (isset($p_values["mime"]) ? $p_values["mime"] : NULL ); - // $storedFile->filepath = $p_values['filepath']; - if (isset($p_values['md5'])) { - $storedFile->md5 = $p_values['md5']; - } elseif (file_exists($p_values['filepath'])) { - //echo "StoredFile::Insert: WARNING: Having to recalculate MD5 value\n"; - $storedFile->md5 = md5_file($p_values['filepath']); - } - - // Check for duplicates -- return duplicate - $duplicate = StoredFile::RecallByMd5($storedFile->md5); - if ($duplicate) { - return $duplicate; - } - - $storedFile->exists = FALSE; - - // Insert record into the database - $escapedName = pg_escape_string($storedFile->name); - $escapedFtype = pg_escape_string($storedFile->ftype); - $sql = "INSERT INTO ".$CC_CONFIG['filesTable'] - ."(id, name, gunid, mime, state, ftype, mtime, md5)" - ."VALUES ({$sqlId}, '{$escapedName}', " - ." '{$storedFile->gunid}'," - ." '{$storedFile->mime}', 'incomplete', '$escapedFtype'," - ." now(), '{$storedFile->md5}')"; - //$_SESSION["debug"] .= "sql: ".$sql."
"; - //echo $sql."\n"; - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - $CC_DBC->query("ROLLBACK"); - return $res; - } - - if (!is_integer($storedFile->id)) { - // NOTE: POSTGRES-SPECIFIC - $sql = "SELECT currval('".$CC_CONFIG["filesSequence"]."_seq')"; - $storedFile->id = $CC_DBC->getOne($sql); - } - $storedFile->setMetadataBatch($metadata); - - // Save media file - $res = $storedFile->addFile($p_values['filepath'], $p_copyMedia); - if (PEAR::isError($res)) { - echo "StoredFile::Insert -- addFile(): '".$res->getMessage()."'\n"; - return $res; - } - - if (empty($storedFile->mime)) { - //echo "StoredFile::Insert: WARNING: Having to recalculate MIME value\n"; - $storedFile->setMime($storedFile->getMime()); - } - - // Save state - $storedFile->setState('ready'); - - // Recall the object to get all the proper values - $storedFile = StoredFile::RecallByGunid($storedFile->gunid); - return $storedFile; + $propelColumn = $this->_dbMD[$p_category]; + $method = "set$propelColumn"; + $this->_file->$method($p_value); + $this->_file->save(); } /** - * Fetch instance of StoreFile object.
- * Should be supplied with only ONE parameter, all the rest should - * be NULL. + * Get one metadata value. * - * @param int $p_id - * local id - * @param string $p_gunid - * global unique id of file - * @param string $p_md5sum - * MD5 sum of the file - * @return StoredFile|Playlist|NULL - * Return NULL if the object doesnt exist in the DB. + * @param string $p_category (MDATA_KEY_URL) + * @return string */ - public static function Recall($p_id=null, $p_gunid=null, $p_md5sum=null, $p_filepath=null) + public function getMetadataValue($p_category) { - global $CC_DBC; - global $CC_CONFIG; - if (!is_null($p_id)) { - $cond = "id='".intval($p_id)."'"; - } elseif (!is_null($p_gunid)) { - $cond = "gunid='$p_gunid'"; - } elseif (!is_null($p_md5sum)) { - $cond = "md5='$p_md5sum'"; - } elseif (!is_null($p_filepath)) { - $cond = "filepath='$p_filepath'"; - } else { - return null; - } - $sql = "SELECT *" - ." FROM ".$CC_CONFIG['filesTable'] - ." WHERE $cond"; - - $row = $CC_DBC->getRow($sql); - if (PEAR::isError($row) || is_null($row)) { - return $row; - } - $gunid = $row['gunid']; - $storedFile = new StoredFile($gunid); - $storedFile->id = $row['id']; - $storedFile->name = $row['name']; - $storedFile->mime = $row['mime']; - $storedFile->ftype = $row['ftype']; - $storedFile->state = $row['state']; - $storedFile->currentlyaccessing = $row['currentlyaccessing']; - $storedFile->editedby = $row['editedby']; - $storedFile->mtime = $row['mtime']; - $storedFile->md5 = $row['md5']; - $storedFile->filepath = $row['filepath']; - - if(file_exists($row['filepath'])) { - $storedFile->exists = true; - } - else { - $storedFile->exists = false; - } - - $storedFile->setFormat($row['ftype']); - return $storedFile; + return $this->getDbColMetadataValue(constant($p_category)); } - /** - * Create instance of StoreFile object and recall existing file - * by gunid. + /** + * Get one metadata value. * - * @param string $p_gunid - * global unique id of file - * @return StoredFile + * @param string $p_category (url) + * @return string */ - public static function RecallByGunid($p_gunid='') + public function getDbColMetadataValue($p_category) { - return StoredFile::Recall(null, $p_gunid); + $propelColumn = $this->_dbMD[$p_category]; + $method = "get$propelColumn"; + return $this->_file->$method(); } - /** - * Fetch the StoredFile by looking up the MD5 value. + * Get metadata as array, indexed by the column names in the database. * - * @param string $p_md5sum - * @return StoredFile|NULL|PEAR_Error + * @return array */ - public static function RecallByMd5($p_md5sum) + public function getDbColMetadata() { - return StoredFile::Recall(null, null, $p_md5sum); + $md = array(); + foreach ($this->_dbMD as $dbColumn => $propelColumn) { + $method = "get$propelColumn"; + $md[$dbColumn] = $this->_file->$method(); + } + + return $md; } - - public static function GetAll() - { - global $CC_CONFIG, $CC_DBC; - $sql = "SELECT * FROM ".$CC_CONFIG["filesTable"]; - $rows = $CC_DBC->GetAll($sql); - return $rows; - } - - private function ensureDir($dir) - { - if (!is_dir($dir)) { - mkdir($dir, 02775); - chmod($dir, 02775); - } - } - - private function createUniqueFilename($base, $ext) - { - if(file_exists("$base.$ext")) { - $i = 1; - while(true) { - if(file_exists("$base($i).$ext")) { - $i = $i+1; - } - else { - return "$base($i).$ext"; - } - } - } - - return "$base.$ext"; - } - - /** - * Generate the location to store the file. - * It creates the subdirectory if needed. - */ - private function generateFilePath() - { - global $CC_CONFIG, $CC_DBC; - - $storageDir = $CC_CONFIG['storageDir']; - $info = pathinfo($this->name); - $origName = $info['filename']; - $fileExt = strtolower($info["extension"]); - - $this->loadMetadata(); - - $artist = $this->md["dc:creator"]; - $album = $this->md["dc:source"]; - $title = $this->md["dc:title"]; - $track_num = $this->md["ls:track_num"]; - - if(isset($artist) && $artist != "") { - $base = "$storageDir/$artist"; - $this->ensureDir($base); - - if(isset($album) && $album != "") { - $base = "$base/$album"; - $this->ensureDir($base); - } - - if(isset($title) && $title != "") { - if(isset($track_num) && $track_num != "") { - if($track_num < 10 && strlen($track_num) == 1) { - $track_num = "0$track_num"; - } - $base = "$base/$track_num - $title"; - } - else { - $base = "$base/$title"; - } - } - else { - $base = "$base/$origName"; - } - } - else { - $base = "$storageDir/$origName"; - } - - return $this->createUniqueFilename($base, $fileExt); - } - - /** - * Insert media file to filesystem - * - * @param string $p_localFilePath - * local path - * @param boolean $p_copyMedia - * copy the media file if true, make symlink if false - * @return TRUE|PEAR_Error - */ - public function addFile($p_localFilePath, $p_copyMedia=TRUE) - { - global $CC_CONFIG, $CC_DBC; - if ($this->exists) { - return FALSE; - } - // for files downloaded from remote instance: - if ($p_localFilePath == $this->filepath) { - $this->exists = TRUE; - return TRUE; - } - umask(0002); - $dstFile = ''; - if ($p_copyMedia) { - $dstFile = $this->generateFilePath(); - $r = @copy($p_localFilePath, $dstFile); - if (!$r) { - $this->exists = FALSE; - return PEAR::raiseError( - "StoredFile::addFile: file save failed". - " ($p_localFilePath, {$this->filepath})",GBERR_FILEIO - ); - } - } else { - $dstFile = $p_localFilePath; - $r = TRUE; - } - $this->filepath = $dstFile; - $sqlPath = pg_escape_string($this->filepath); - $sql = "UPDATE ".$CC_CONFIG["filesTable"] - ." SET filepath='{$sqlPath}'" - ." WHERE id={$this->id}"; - //echo $sql."\n"; - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - $this->exists = TRUE; - return TRUE; - } - - - /** - * Find and return the first exact match for the original file name - * that was used on import. - * @param string $p_name - */ - public static function findByOriginalName($p_name) - { - global $CC_CONFIG, $CC_DBC; - $sql = "SELECT id FROM ".$CC_CONFIG["filesTable"] - ." WHERE name='".pg_escape_string($p_name)."'"; - $id = $CC_DBC->getOne($sql); - if (is_numeric($id)) { - return StoredFile::Recall($id); - } else { - return NULL; - } - } - - /** * Delete and insert media file * @@ -954,192 +214,6 @@ class StoredFile { return $this->addFile($p_localFilePath, $p_copyMedia); } - - /** - * Return true if file corresponding to the object exists - * - * @return boolean - */ - public function existsFile() - { - return $this->exists; - } - - - /** - * Create instance of StoredFile object and make copy of existing file - * - * @param StoredFile $p_src - * source object - * @param int $p_nid - * new local id - * @return StoredFile - */ - public static function CopyOf(&$p_src, $p_nid) - { - $values = array( - "id" => $p_nid, - "filename" => $p_src->name, - "filepath" => $p_src->getRealFilePath(), - "filetype" => $p_src->getType() - ); - $storedFile = StoredFile::Insert($values); - if (PEAR::isError($storedFile)) { - return $storedFile; - } - $storedFile->replaceMetadata($p_src->getAllMetadata(), 'string'); - return $storedFile; - } - - - private static function NormalizeExtent($v) - { - if (!preg_match("|^\d{2}:\d{2}:\d{2}.\d{6}$|", $v)) { - $s = Playlist::playlistTimeToSeconds($v); - $t = Playlist::secondsToPlaylistTime($s); - return $t; - } - return $v; - } - - /** - * Set metadata element value - * - * @param string $category - * Metadata element identification (e.g. dc:title) - * @param string $value - * value to store, if NULL then delete record - * @return boolean - */ - public function setMetadataValue($p_category, $p_value) - { - global $CC_CONFIG, $CC_DBC; - if (!is_string($p_category) || is_array($p_value)) { - return FALSE; - } - if ($p_category == 'dcterms:extent') { - $p_value = StoredFile::NormalizeExtent($p_value); - } - $columnName = StoredFile::xmlCategoryToDbColumn($p_category); // Get column name - - if (!is_null($columnName)) { - $escapedValue = pg_escape_string($p_value); - $sql = "UPDATE ".$CC_CONFIG["filesTable"] - ." SET $columnName='$escapedValue'" - ." WHERE id={$this->id}"; - //var_dump($sql); - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - } - return TRUE; - } - - - /** - * Set metadata values in 'batch' mode - * - * @param array $values - * array of key/value pairs - * (e.g. 'dc:title'=>'New title') - * @return boolean - */ - public function setMetadataBatch($values) - { - global $CC_CONFIG, $CC_DBC; - if (!is_array($values)) { - $values = array($values); - } - if (count($values) == 0) { - return true; - } - foreach ($values as $category => $oneValue) { - $columnName = StoredFile::xmlCategoryToDbColumn($category); - if (!is_null($columnName)) { - if ($category == 'dcterms:extent') { - $oneValue = StoredFile::NormalizeExtent($oneValue); - } - // Since track_number is an integer, you cannot set - // it to be the empty string, so we NULL it instead. - if ($columnName == 'track_number' && empty($oneValue)) { - $sqlPart = "$columnName = NULL"; - } elseif (($columnName == 'length') && (strlen($oneValue) > 8)) { - // Postgres doesnt like it if you try to store really large hour - // values. TODO: We need to fix the underlying problem of getting the - // right values. - $parts = explode(':', $oneValue); - $hour = intval($parts[0]); - if ($hour > 24) { - continue; - } else { - $sqlPart = "$columnName = '$oneValue'"; - } - } else { - $escapedValue = pg_escape_string($oneValue); - $sqlPart = "$columnName = '$escapedValue'"; - } - $sqlValues[] = $sqlPart; - } - } - if (count($sqlValues)==0) { - return TRUE; - } - $sql = "UPDATE ".$CC_CONFIG["filesTable"] - ." SET ".join(",", $sqlValues) - ." WHERE id={$this->id}"; - $CC_DBC->query($sql); - return TRUE; - } - - - /** - * Get metadata as array, indexed by the column names in the database. - * - * @return array - */ - public function getMetadata() - { - return $this->md; - } - - /** - * Get one metadata value. - * - * @param string $p_name - * @return string - */ - public function getMetadataValue($p_name) - { - if (isset($this->md[$p_name])){ - return $this->md[$p_name]; - } else { - return ""; - } - } - - /** - * Rename stored virtual file - * - * @param string $p_newname - * @return TRUE|PEAR_Error - */ - public function setName($p_newname) - { - global $CC_CONFIG, $CC_DBC; - $escapedName = pg_escape_string($p_newname); - $sql = "UPDATE ".$CC_CONFIG['filesTable'] - ." SET name='$escapedName', mtime=now()" - ." WHERE gunid='{$this->gunid}'"; - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - $this->name = $p_newname; - return TRUE; - } - - /** * Set state of virtual file * @@ -1166,156 +240,6 @@ class StoredFile { return TRUE; } - /** - * Set mime-type of virtual file - * - * @param string $p_mime - * mime-type - * @return boolean|PEAR_Error - */ - public function setMime($p_mime) - { - global $CC_CONFIG, $CC_DBC; - if (!is_string($p_mime)) { - $p_mime = 'application/octet-stream'; - } - $escapedMime = pg_escape_string($p_mime); - $sql = "UPDATE ".$CC_CONFIG['filesTable'] - ." SET mime='$escapedMime', mtime=now()" - ." WHERE gunid='{$this->gunid}'"; - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - $this->mime = $p_mime; - return TRUE; - } - - - /** - * Set md5 of virtual file - * - * @param string $p_md5sum - * @return boolean|PEAR_Error - */ - public function setMd5($p_md5sum) - { - global $CC_CONFIG, $CC_DBC; - $escapedMd5 = pg_escape_string($p_md5sum); - $sql = "UPDATE ".$CC_CONFIG['filesTable'] - ." SET md5='$escapedMd5', mtime=now()" - ." WHERE gunid='{$this->gunid}'"; - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - $this->md5 = $p_md5sum; - return TRUE; - } - - /** - * Delete media file from filesystem. - * You cant delete a file if it is being accessed. - * You cant delete a file if it is scheduled to be played in the future. - * The file will be removed from all playlists it is a part of. - * - * @return boolean|PEAR_Error - */ - public function deleteFile() - { - global $CC_CONFIG; - if (!$this->exists) { - return FALSE; - } - if ($this->isAccessed()) { - return PEAR::raiseError( - 'Cannot delete a file that is currently accessed.' - ); - } - - // Check if the file is scheduled to be played in the future - if (Schedule::IsFileScheduledInTheFuture($this->id)) { - return PEAR::raiseError( - 'Cannot delete a file that is scheduled in the future.' - ); - } - - // Only delete the file from filesystem if it has been copied to the - // storage directory. (i.e. dont delete linked files) - if (substr($this->filepath, 0, strlen($CC_CONFIG["storageDir"])) == $CC_CONFIG["storageDir"]) { - // Delete the file - if (!file_exists($this->filepath) || @unlink($this->filepath)) { - $this->exists = FALSE; - return TRUE; - } - else { - return PEAR::raiseError("StoredFile::deleteFile: unlink failed ({$this->filepath})"); - } - } - else { - $this->exists = FALSE; - return TRUE; - } - } - - - /** - * Delete stored virtual file - * - * @param boolean $p_deleteFile - * - * @return TRUE|PEAR_Error - */ - public function delete($p_deleteFile = true) - { - global $CC_CONFIG, $CC_DBC; - if ($p_deleteFile) { - $res = $this->deleteFile(); - if (PEAR::isError($res)) { - return $res; - } - - Playlist::DeleteFileFromAllPlaylists($this->id); - } - - $sql = "DELETE FROM ".$CC_CONFIG['filesTable'] - ." WHERE gunid='{$this->gunid}'"; - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - return TRUE; - } - - - public static function deleteById($p_id) - { - global $CC_CONFIG, $CC_DBC; - if (!is_numeric($p_id)) { - return FALSE; - } - $sql = "DELETE FROM ".$CC_CONFIG["filesTable"]." WHERE id=$p_id"; - - $res = $CC_DBC->query($sql); - if (PEAR::isError($res)) { - return $res; - } - return TRUE; - } - - public static function deleteAll() - { - global $CC_CONFIG, $CC_DBC; - $files = StoredFile::getAll(); - foreach ($files as $file) { - $media = StoredFile::Recall($file["id"]); - $result = $media->delete(); - if (PEAR::isError($result)) { - return $result; - } - } - } - /** * Returns an array of playlist objects that this file is a part of. * @return array @@ -1335,255 +259,125 @@ class StoredFile { return $playlists; } - /** - * Returns true if virtual file is currently in use.
- * Static or dynamic call is possible. + * Delete stored virtual file * - * @param string $p_gunid - * optional (for static call), global unique id - * @return boolean|PEAR_Error + * @param boolean $p_deleteFile + * + * @return void|PEAR_Error */ - public function isAccessed($p_gunid=NULL) + public function delete() { - global $CC_CONFIG, $CC_DBC; - if (is_null($p_gunid)) { - return ($this->currentlyaccessing > 0); + if ($this->exists()) { + if ($this->getFormat() == 'audioclip') { + $res = $this->deleteFile(); + if (PEAR::isError($res)) { + return $res; + } + } } - $sql = "SELECT currentlyAccessing FROM ".$CC_CONFIG['filesTable'] - ." WHERE gunid='$p_gunid'"; - $ca = $CC_DBC->getOne($sql); - if (is_null($ca)) { - return PEAR::raiseError( - "StoredFile::isAccessed: invalid gunid ($p_gunid)", - GBERR_FOBJNEX - ); - } - return ($ca > 0); + + Playlist::DeleteFileFromAllPlaylists($this->getId()); + $this->_file->delete(); } + /** + * Delete media file from filesystem. + * You cant delete a file if it is being accessed. + * You cant delete a file if it is scheduled to be played in the future. + * The file will be removed from all playlists it is a part of. + * + * @return void|PEAR_Error + */ + public function deleteFile() + { + global $CC_CONFIG; + + if ($this->isAccessed()) { + return PEAR::raiseError('Cannot delete a file that is currently accessed.'); + } + + // Check if the file is scheduled to be played in the future + if (Schedule::IsFileScheduledInTheFuture($this->getId())) { + return PEAR::raiseError('Cannot delete a file that is scheduled in the future.'); + } + + // Only delete the file from filesystem if it has been copied to the storage directory + if (substr($this->getFilePath(), 0, strlen($CC_CONFIG["storageDir"])) == $CC_CONFIG["storageDir"]) { + // Delete the file + $res = unlink($this->getFilePath()); + if (!$res) { + return PEAR::raiseError("StoredFile::deleteFile: unlink failed ({$this->getFilePath()})"); + } + } + } /** - * Returns true if virtual file is edited - * - * @param string $p_playlistId - * playlist global unique ID + * Returns true if media file exists * @return boolean */ - public function isEdited($p_playlistId=NULL) - { - if (is_null($p_playlistId)) { - return ($this->state == 'edited'); - } - $state = $this->getState($p_playlistId); - if ($state != 'edited') { - return FALSE; - } - return TRUE; - } - - - /** - * Returns id of user editing playlist - * - * @param string $p_playlistId - * playlist global unique ID - * @return int|null|PEAR_Error - * id of user editing it - */ - public function isEditedBy($p_playlistId=NULL) - { - global $CC_CONFIG, $CC_DBC; - if (is_null($p_playlistId)) { - $p_playlistId = $this->gunid; - } - $sql = "SELECT editedBy FROM ".$CC_CONFIG['filesTable'] - ." WHERE gunid='$p_playlistId'"; - $ca = $CC_DBC->getOne($sql); - if (PEAR::isError($ca)) { - return $ca; - } - if (is_null($ca)) { - return $ca; - } - return intval($ca); - } - - - /** - * Return local ID of virtual file. - * - * @return int - */ - public function getId() - { - return $this->id; - } - - - /** - * Return global ID of virtual file. - * - * @return string - */ - public function getGunid() - { - return $this->gunid; - } - - - /** - * Returns true if raw media file exists - * @return boolean|PEAR_Error - */ public function exists() { - global $CC_CONFIG, $CC_DBC; - $sql = "SELECT gunid " - ." FROM ".$CC_CONFIG['filesTable'] - ." WHERE gunid='{$this->gunid}'"; - $indb = $CC_DBC->getRow($sql); - if (PEAR::isError($indb)) { - return $indb; + if ($this->_file->isDeleted()) { + return false; } - if (is_null($indb)) { - return FALSE; - } - if ($this->ftype == 'audioclip') { + if ($this->getFormat() == 'audioclip') { return $this->existsFile(); } - return TRUE; } - /** - * Create new global unique id - * @return string + * Returns true if raw media file exists + * @return boolean */ - public static function generateGunid() - { - return md5(uniqid("", true)); + public function existsFile() { + + $filepath = $this->_file->getDbFilepath(); + + if (!isset($filepath) || !file_exists($filepath) || !is_readable($filepath)) { + return false; + } + else { + return true; + } } + /** + * Returns true if virtual file is currently in use.
+ * + * @return boolean + */ + public function isAccessed() + { + return ($this->_file->getDbCurrentlyaccessing() > 0); + } /** * Return suitable extension. * - * @todo make it general - is any tool for it? - * * @return string * file extension without a dot */ public function getFileExtension() { - $fname = $this->getName(); - $pos = strrpos($fname, '.'); - if ($pos !== FALSE) { - $ext = substr($fname, $pos+1); - if ($ext !== FALSE) { - return $ext; - } + $mime = $this->_file->getDbMime(); + + if ($mime == "audio/vorbis") { + return "ogg"; } - switch (strtolower($this->mime)) { - case "audio/mpeg": - $ext = "mp3"; - break; - case "audio/x-wav": - case "audio/x-wave": - $ext = "wav"; - break; - case "audio/x-ogg": - case "application/x-ogg": - $ext = "ogg"; - break; - default: - $ext = "bin"; - break; + else if ($mime == "audio/mp3") { + return "mp3"; } - return $ext; } - - /** - * Get mime-type stored in the file. - * Warning: this function is slow! - * - * @return string - */ - public function getMime() - { - $a = Metadata::LoadFromFile($this->filepath); - if (PEAR::isError($a)) { - return $a; - } - if (isset($a['dc:format'])) { - return $a['dc:format']; - } - return ''; - } - - - /** - * Convenience function. - * @return string - */ - public function getTitle() - { - return $this->md["title"]; - } - - public function getType() - { - return $this->ftype; - } - - /** - * Get storage-internal file state - * - * @param string $p_gunid - * global unique id of file - * @return string - * see install() - */ - public function getState($p_gunid=NULL) - { - global $CC_CONFIG, $CC_DBC; - if (is_null($p_gunid)) { - return $this->state; - } - $sql = "SELECT state FROM ".$CC_CONFIG['filesTable'] - ." WHERE gunid='$p_gunid'"; - return $CC_DBC->getOne($sql); - } - - - /** - * Get mnemonic file name - * - * @param string $p_gunid - * global unique id of file - * @return string - */ - public function getName($p_gunid=NULL) - { - global $CC_CONFIG, $CC_DBC; - if (is_null($p_gunid)) { - return $this->name; - } - $sql = "SELECT name FROM ".$CC_CONFIG['filesTable'] - ." WHERE gunid='$p_gunid'"; - return $CC_DBC->getOne($sql); - } - - /** * Get real filename of raw media data * * @return string */ - public function getRealFilePath() + public function getFilePath() { - return $this->filepath; + return $this->_file->getDbFilepath(); } /** @@ -1592,65 +386,138 @@ class StoredFile { public function getFileUrl() { global $CC_CONFIG; - return "http://$CC_CONFIG[baseUrl]:$CC_CONFIG[basePort]/api/get-media/file/".$this->gunid.".".$this->getFileExtension(); + return "http://$CC_CONFIG[baseUrl]:$CC_CONFIG[basePort]/api/get-media/file/".$this->getGunId().".".$this->getFileExtension(); + } + + public static function Insert($md=null) + { + $file = new CcFiles(); + $file->setDbGunid(md5(uniqid("", true))); + $file->save(); + + $storedFile = new StoredFile(); + $storedFile->_file = $file; + + if(isset($md)) { + $storedFile->setMetadata($md); + } + + return $storedFile; } /** - * Get real filename of metadata file + * Fetch instance of StoreFile object.
+ * Should be supplied with only ONE parameter, all the rest should + * be NULL. * - * @return string - * @see MetaData + * @param int $p_id + * local id + * @param string $p_gunid + * global unique id of file + * @param string $p_md5sum + * MD5 sum of the file + * @return StoredFile|NULL + * Return NULL if the object doesnt exist in the DB. */ - public function getRealMetadataFileName() + public static function Recall($p_id=null, $p_gunid=null, $p_md5sum=null, $p_filepath=null) { - //return $this->md->getFileName(); - return $this->md["name"]; + if (isset($p_id)) { + $file = CcFilesQuery::create()->findPK(intval($p_id)); + } + else if (isset($p_gunid)) { + $file = CcFilesQuery::create() + ->filterByDbGunid($p_gunid) + ->findOne(); + } + else if (isset($p_md5sum)) { + $file = CcFilesQuery::create() + ->filterByDbMd5($p_md5sum) + ->findOne(); + } + else if (isset($p_filepath)) { + $file = CcFilesQuery::create() + ->filterByDbFilepath($p_filepath) + ->findOne(); + } + else { + return null; + } + + if (isset($file)) { + $storedFile = new StoredFile(); + $storedFile->_file = $file; + + return $storedFile; + } + else { + return null; + } + } + + /** + * Create instance of StoreFile object and recall existing file + * by gunid. + * + * @param string $p_gunid + * global unique id of file + * @return StoredFile|NULL + */ + public static function RecallByGunid($p_gunid) + { + return StoredFile::Recall(null, $p_gunid); } /** - * Create and return name for temporary symlink. + * Fetch the StoredFile by looking up the MD5 value. * - * @todo Should be more unique - * @return string + * @param string $p_md5sum + * @return StoredFile|NULL */ - private function _getAccessFileName($p_token, $p_ext='EXT') + public static function RecallByMd5($p_md5sum) { - global $CC_CONFIG; - return $CC_CONFIG['accessDir']."/$p_token.$p_ext"; + return StoredFile::Recall(null, null, $p_md5sum); } + /** + * Fetch the StoredFile by looking up its filepath. + * + * @param string $p_filepath path of file stored in Airtime. + * @return StoredFile|NULL + */ + public static function RecallByFilepath($p_filepath) + { + return StoredFile::Recall(null, null, null, $p_filepath); + } public static function searchFilesForPlaylistBuilder($datatables) { global $CC_CONFIG; + $displayData = array("track_title", "artist_name", "album_title", "track_number", "length", "ftype"); + $plSelect = "SELECT "; $fileSelect = "SELECT "; - foreach (Metadata::GetMapMetadataXmlToDb() as $key => $val){ + foreach ($displayData as $key){ - if($key === "dc:title"){ - $plSelect .= "name AS ".$val.", "; - $fileSelect .= $val.", "; + if($key === "track_title"){ + $plSelect .= "name AS ".$key.", "; + $fileSelect .= $key.", "; } - else if ($key === "ls:type"){ - $plSelect .= "'playlist' AS ".$val.", "; - $fileSelect .= $val.", "; + else if ($key === "ftype"){ + $plSelect .= "'playlist' AS ".$key.", "; + $fileSelect .= $key.", "; } - else if ($key === "dc:creator"){ - $plSelect .= "creator AS ".$val.", "; - $fileSelect .= $val.", "; + else if ($key === "artist_name"){ + $plSelect .= "creator AS ".$key.", "; + $fileSelect .= $key.", "; } - else if ($key === "dcterms:extent"){ - $plSelect .= "length, "; - $fileSelect .= "length, "; - } - else if ($key === "dc:description"){ - $plSelect .= "text(description) AS ".$val.", "; - $fileSelect .= $val.", "; + else if ($key === "length"){ + $plSelect .= $key.", "; + $fileSelect .= $key.", "; } else { - $plSelect .= "NULL AS ".$val.", "; - $fileSelect .= $val.", "; + $plSelect .= "NULL AS ".$key.", "; + $fileSelect .= $key.", "; } } @@ -1754,7 +621,7 @@ class StoredFile { return array("sEcho" => intval($data["sEcho"]), "iTotalDisplayRecords" => $totalDisplayRows, "iTotalRecords" => $totalRows, "aaData" => $results); } - public static function uploadFile($p_targetDir) + public static function uploadFile($p_targetDir) { // HTTP headers for no cache etc header('Content-type: text/plain; charset=UTF-8'); @@ -1765,7 +632,6 @@ class StoredFile { header("Pragma: no-cache"); // Settings - //$p_targetDir = ini_get("upload_tmp_dir"); //. DIRECTORY_SEPARATOR . "plupload"; $cleanupTargetDir = false; // Remove old files $maxFileAge = 60 * 60; // Temp file age in seconds @@ -1866,35 +732,22 @@ class StoredFile { } } } + else { - $metadata = Metadata::LoadFromFile($audio_file); + global $CC_CONFIG; + $stor = $CC_CONFIG["storageDir"]; + $audio_stor = $stor . DIRECTORY_SEPARATOR . $fileName; - if (PEAR::isError($metadata)) { - die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": ' + $metadata->getMessage() + '}}'); + $md = array(); + $md['MDATA_KEY_MD5'] = $md5; + $md['MDATA_KEY_FILEPATH'] = $audio_stor; + $md['MDATA_KEY_TITLE'] = $fileName; + + StoredFile::Insert($md); + $r = @chmod($audio_file, 0666); + $r = @rename($audio_file, $audio_stor); } - // no id3 title tag -> use the original filename for title - if (empty($metadata[UI_MDATA_KEY_TITLE])) { - $metadata[UI_MDATA_KEY_TITLE] = basename($audio_file); - $metadata[UI_MDATA_KEY_FILENAME] = basename($audio_file); - } - - $values = array( - "filename" => basename($audio_file), - "filepath" => $audio_file, - "filetype" => "audioclip", - "mime" => $metadata[UI_MDATA_KEY_FORMAT], - "md5" => $md5 - ); - $storedFile = StoredFile::Insert($values); - - if (PEAR::isError($storedFile)) { - die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": ' + $storedFile->getMessage() + '}}'); - } - - $storedFile->setMetadataBatch($metadata); - - return $storedFile; } } diff --git a/install/airtime-install b/install/airtime-install index b86f03ad8..bc2187d68 100755 --- a/install/airtime-install +++ b/install/airtime-install @@ -11,11 +11,11 @@ SCRIPTPATH=`dirname $SCRIPT` echo -e "\n******************************** Install Begin *********************************" -php ${SCRIPTPATH}/airtime-install.php $@ - echo -e "\n*** Creating Pypo User ***" python ${SCRIPTPATH}/../python_apps/create-pypo-user.py +php ${SCRIPTPATH}/airtime-install.php $@ + echo -e "\n*** Pypo Installation ***" python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-install.py diff --git a/install/airtime-install.php b/install/airtime-install.php index 8a0a6f741..cb4a0c90f 100644 --- a/install/airtime-install.php +++ b/install/airtime-install.php @@ -121,8 +121,6 @@ if ($db_install) { AirtimeInstall::InstallStorageDirectory(); -AirtimeInstall::ChangeDirOwnerToWebserver($CC_CONFIG["storageDir"]); - AirtimeInstall::CreateSymlinksToUtils(); AirtimeInstall::CreateZendPhpLogFile(); diff --git a/install/include/AirtimeInstall.php b/install/include/AirtimeInstall.php index c1d4a9e7a..dbbff57b6 100644 --- a/install/include/AirtimeInstall.php +++ b/install/include/AirtimeInstall.php @@ -116,42 +116,39 @@ class AirtimeInstall } } - public static function ChangeDirOwnerToWebserver($filePath) - { - global $CC_CONFIG; - echo "* Giving Apache permission to access $filePath".PHP_EOL; - - $success = chgrp($filePath, $CC_CONFIG["webServerUser"]); - $fileperms=@fileperms($filePath); - $fileperms = $fileperms | 0x0010; // group write bit - $fileperms = $fileperms | 0x0400; // group sticky bit - chmod($filePath, $fileperms); - } - public static function InstallStorageDirectory() { global $CC_CONFIG, $CC_DBC; echo "* Storage directory setup".PHP_EOL; - foreach (array('baseFilesDir', 'storageDir') as $d) { - if ( !file_exists($CC_CONFIG[$d]) ) { - @mkdir($CC_CONFIG[$d], 02775, true); - if (file_exists($CC_CONFIG[$d])) { - $rp = realpath($CC_CONFIG[$d]); - echo "* Directory $rp created".PHP_EOL; - } else { - echo "* Failed creating {$CC_CONFIG[$d]}".PHP_EOL; - exit(1); - } - } elseif (is_writable($CC_CONFIG[$d])) { - $rp = realpath($CC_CONFIG[$d]); - echo "* Skipping directory already exists: $rp".PHP_EOL; + $stor_dir = $CC_CONFIG['storageDir']; + + if (!file_exists($stor_dir)) { + @mkdir($stor_dir, 02777, true); + if (file_exists($stor_dir)) { + $rp = realpath($stor_dir); + echo "* Directory $rp created".PHP_EOL; } else { - $rp = realpath($CC_CONFIG[$d]); - echo "* WARNING: Directory already exists, but is not writable: $rp".PHP_EOL; + echo "* Failed creating {$stor_dir}".PHP_EOL; + exit(1); } - $CC_CONFIG[$d] = $rp; } + else if (is_writable($stor_dir)) { + $rp = realpath($stor_dir); + echo "* Skipping directory already exists: $rp".PHP_EOL; + } + else { + $rp = realpath($stor_dir); + echo "* WARNING: Directory already exists, but is not writable: $rp".PHP_EOL; + return; + } + + echo "* Giving Apache permission to access $rp".PHP_EOL; + $success = chgrp($rp, $CC_CONFIG["webServerUser"]); + $success = chown($rp, "pypo"); + $success = chmod($rp, 02777); + $CC_CONFIG['storageDir'] = $rp; + } public static function CreateDatabaseUser() @@ -300,7 +297,7 @@ class AirtimeInstall echo "* Installing airtime-update-db-settings".PHP_EOL; $dir = AirtimeInstall::CONF_DIR_BINARIES."/utils/airtime-update-db-settings"; exec("ln -s $dir /usr/bin/airtime-update-db-settings"); - + echo "* Installing airtime-check-system".PHP_EOL; $dir = AirtimeInstall::CONF_DIR_BINARIES."/utils/airtime-check-system"; exec("ln -s $dir /usr/bin/airtime-check-system"); diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index 84e1a4d43..016041255 100644 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -117,6 +117,9 @@ class ApiClientInterface: def upload_recorded_show(self): pass + def check_media_status(self, md5): + pass + def update_media_metadata(self, md): pass @@ -356,13 +359,52 @@ class AirTimeApiClient(ApiClientInterface): return response - def update_media_metadata(self, md): + def setup_media_monitor(self): + 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["media_setup_url"]) + url = url.replace("%%api_key%%", self.config["api_key"]) + logger.debug(url) + + response = urllib.urlopen(url) + response = json.loads(response.read()) + logger.debug("Json Media Setup %s", response) + + except Exception, e: + response = None + logger.error("Exception: %s", e) + + return response + + def check_media_status(self, md5): + 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["media_status_url"]) + url = url.replace("%%api_key%%", self.config["api_key"]) + url = url.replace("%%md5%%", md5) + logger.debug(url) + + response = urllib.urlopen(url) + response = json.loads(response.read()) + logger.info("Json Media Status %s", response) + + except Exception, e: + logger.error("Exception: %s", e) + + return response + + def update_media_metadata(self, md, mode): 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"]) + url = url.replace("%%mode%%", mode) data = recursive_urlencode(md) req = urllib2.Request(url, data) @@ -372,6 +414,7 @@ class AirTimeApiClient(ApiClientInterface): response = json.loads(response) except Exception, e: + response = None logger.error("Exception: %s", e) return response diff --git a/python_apps/media-monitor/MediaMonitor.py b/python_apps/media-monitor/MediaMonitor.py index 3143307c2..a909e2874 100644 --- a/python_apps/media-monitor/MediaMonitor.py +++ b/python_apps/media-monitor/MediaMonitor.py @@ -9,7 +9,10 @@ import sys import hashlib import json import shutil +import math +from collections import deque +from pwd import getpwnam from subprocess import Popen, PIPE, STDOUT from configobj import ConfigObj @@ -23,8 +26,13 @@ from kombu.connection import BrokerConnection from kombu.messaging import Exchange, Queue, Consumer, Producer from api_clients import api_client +MODE_CREATE = "create" +MODE_MODIFY = "modify" +MODE_MOVED = "moved" +MODE_DELETE = "delete" + global storage_directory -storage_directory = "/srv/airtime/stor" +global plupload_directory # configure logging try: @@ -46,35 +54,27 @@ 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'] """ - -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 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",\ + "MDATA_KEY_TITLE": "title",\ + "MDATA_KEY_CREATOR": "artist",\ + "MDATA_KEY_SOURCE": "album",\ + "MDATA_KEY_GENRE": "genre",\ + "MDATA_KEY_MOOD": "mood",\ + "MDATA_KEY_TRACKNUMBER": "tracknumber",\ + "MDATA_KEY_BPM": "bpm",\ + "MDATA_KEY_LABEL": "organization",\ + "MDATA_KEY_COMPOSER": "composer",\ + "MDATA_KEY_ENCODER": "encodedby",\ + "MDATA_KEY_CONDUCTOR": "conductor",\ + "MDATA_KEY_YEAR": "date",\ + "MDATA_KEY_URL": "website",\ + "MDATA_KEY_ISRC": "isrc",\ + "MDATA_KEY_COPYRIGHT": "copyright",\ } schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True) @@ -112,27 +112,48 @@ class MediaMonitor(ProcessEvent): 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",\ + "title": "MDATA_KEY_TITLE",\ + "artist": "MDATA_KEY_CREATOR",\ + "album": "MDATA_KEY_SOURCE",\ + "genre": "MDATA_KEY_GENRE",\ + "mood": "MDATA_KEY_MOOD",\ + "tracknumber": "MDATA_KEY_TRACKNUMBER",\ + "bpm": "MDATA_KEY_BPM",\ + "organization": "MDATA_KEY_LABEL",\ + "composer": "MDATA_KEY_COMPOSER",\ + "encodedby": "MDATA_KEY_ENCODER",\ + "conductor": "MDATA_KEY_CONDUCTOR",\ + "date": "MDATA_KEY_YEAR",\ + "website": "MDATA_KEY_URL",\ + "isrc": "MDATA_KEY_ISRC",\ + "copyright": "MDATA_KEY_COPYRIGHT",\ } self.supported_file_formats = ['mp3', 'ogg'] self.logger = logging.getLogger('root') self.temp_files = {} - self.imported_renamed_files = {} + self.moved_files = {} + self.file_events = deque() + + self.mask = pyinotify.IN_CREATE | \ + pyinotify.IN_MODIFY | \ + pyinotify.IN_MOVED_FROM | \ + pyinotify.IN_MOVED_TO | \ + pyinotify.IN_DELETE | \ + pyinotify.IN_DELETE_SELF + + self.wm = WatchManager() + + schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True) + schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem") + connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/") + channel = connection.channel() + + def watch_directory(self, directory): + return self.wm.add_watch(directory, self.mask, rec=True, auto_add=True) + + def is_parent_directory(self, filepath, directory): + return (directory == filepath[0:len(directory)]) def get_md5(self, filepath): f = open(filepath, 'rb') @@ -142,20 +163,40 @@ class MediaMonitor(ProcessEvent): return md5 - def ensure_dir(self, filepath): + ## mutagen_length is in seconds with the format (d+).dd + ## return format hh:mm:ss.uuu + def format_length(self, mutagen_length): + t = float(mutagen_length) + h = int(math.floor(t/3600)) + t = t % 3600 + m = int(math.floor(t/60)) + s = t % 60 + # will be ss.uuu + s = str(s) + s = s[:6] + + length = "%s:%s:%s" % (h, m, s) + + return length + + def ensure_dir(self, filepath): directory = os.path.dirname(filepath) - if ((not os.path.exists(directory)) or ((os.path.exists(directory) and not os.path.isdir(directory)))): - os.makedirs(directory, 02775) + try: + omask = os.umask(0) + if ((not os.path.exists(directory)) or ((os.path.exists(directory) and not os.path.isdir(directory)))): + os.makedirs(directory, 02777) + self.watch_directory(directory) + finally: + os.umask(omask) def create_unique_filename(self, filepath): - file_dir = os.path.dirname(filepath) - filename = os.path.basename(filepath).split(".")[0] - file_ext = os.path.splitext(filepath)[1] - if(os.path.exists(filepath)): + file_dir = os.path.dirname(filepath) + filename = os.path.basename(filepath).split(".")[0] + file_ext = os.path.splitext(filepath)[1] i = 1; while(True): new_filepath = "%s/%s(%s).%s" % (file_dir, filename, i, file_ext) @@ -165,79 +206,119 @@ class MediaMonitor(ProcessEvent): else: filepath = new_filepath - self.imported_renamed_files[filepath] = 0 - return filepath def create_file_path(self, imported_filepath): global storage_directory - original_name = os.path.basename(imported_filepath) - file_ext = os.path.splitext(imported_filepath)[1] - file_info = mutagen.File(imported_filepath, easy=True) - - metadata = {'artist':None, - 'album':None, - 'title':None, - 'tracknumber':None} - - for key in metadata.keys(): - if key in file_info: - metadata[key] = file_info[key][0] - - if metadata['artist'] is not None: - base = "%s/%s" % (storage_directory, metadata['artist']) - if metadata['album'] is not None: - base = "%s/%s" % (base, metadata['album']) - if metadata['title'] is not None: - if metadata['tracknumber'] is not None: - metadata['tracknumber'] = "%02d" % (int(metadata['tracknumber'])) - base = "%s/%s - %s" % (base, metadata['tracknumber'], metadata['title']) - else: - base = "%s/%s" % (base, metadata['title']) - else: - base = "%s/%s" % (base, original_name) - else: - base = "%s/%s" % (storage_directory, original_name) - - base = "%s%s" % (base, file_ext) - - filepath = self.create_unique_filename(base) - self.ensure_dir(filepath) - shutil.move(imported_filepath, filepath) - - def update_airtime(self, event): - self.logger.info("Updating Change to Airtime") try: - md5 = self.get_md5(event.pathname) - md = {'filepath':event.pathname, 'md5':md5} + #get rid of file extention from original name, name might have more than 1 '.' in it. + original_name = os.path.basename(imported_filepath) + #self.logger.info('original name: %s', original_name) + original_name = original_name.split(".")[0:-1] + #self.logger.info('original name: %s', original_name) + original_name = ''.join(original_name) + #self.logger.info('original name: %s', original_name) - 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] + file_ext = os.path.splitext(imported_filepath)[1] + file_info = mutagen.File(imported_filepath, easy=True) - data = {'md': md} - response = self.api_client.update_media_metadata(data) + metadata = {'artist':None, + 'album':None, + 'title':None, + 'tracknumber':None} + + for key in metadata.keys(): + if key in file_info: + metadata[key] = file_info[key][0] + + if metadata['artist'] is not None: + base = "%s/%s" % (storage_directory, metadata['artist']) + if metadata['album'] is not None: + base = "%s/%s" % (base, metadata['album']) + if metadata['title'] is not None: + if metadata['tracknumber'] is not None: + metadata['tracknumber'] = "%02d" % (int(metadata['tracknumber'])) + base = "%s/%s - %s" % (base, metadata['tracknumber'], metadata['title']) + else: + base = "%s/%s" % (base, metadata['title']) + else: + base = "%s/%s" % (base, original_name) + else: + base = "%s/%s" % (storage_directory, original_name) + + base = "%s%s" % (base, file_ext) + + filepath = self.create_unique_filename(base) + self.ensure_dir(filepath) except Exception, e: - self.logger.info("%s", e) + self.logger.error('Exception: %s', e) - def is_renamed_file(self, filename): - if filename in self.imported_renamed_files: - del self.imported_renamed_files[filename] - return True + return filepath - return False + def get_mutagen_info(self, filepath): + md = {} + md5 = self.get_md5(filepath) + md['MDATA_KEY_MD5'] = md5 + + file_info = mutagen.File(filepath, easy=True) + attrs = self.mutagen2airtime + for key in file_info.keys() : + if key in attrs : + md[attrs[key]] = file_info[key][0] + + md['MDATA_KEY_BITRATE'] = file_info.info.bitrate + md['MDATA_KEY_SAMPLERATE'] = file_info.info.sample_rate + md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length) + md['MDATA_KEY_MIME'] = file_info.mime[0] + + if "mp3" in md['MDATA_KEY_MIME']: + md['MDATA_KEY_FTYPE'] = "audioclip" + elif "vorbis" in md['MDATA_KEY_MIME']: + md['MDATA_KEY_FTYPE'] = "audioclip" + + return md + + + def update_airtime(self, d): + + filepath = d['filepath'] + mode = d['mode'] + + data = None + md = {} + md['MDATA_KEY_FILEPATH'] = filepath + + if (os.path.exists(filepath) and (mode == MODE_CREATE)): + mutagen = self.get_mutagen_info(filepath) + md.update(mutagen) + data = {'md': md} + elif (os.path.exists(filepath) and (mode == MODE_MODIFY)): + mutagen = self.get_mutagen_info(filepath) + md.update(mutagen) + data = {'md': md} + elif (mode == MODE_MOVED): + mutagen = self.get_mutagen_info(filepath) + md.update(mutagen) + data = {'md': md} + elif (mode == MODE_DELETE): + data = {'md': md} + + if data is not None: + self.logger.info("Updating Change to Airtime") + response = None + while response is None: + response = self.api_client.update_media_metadata(data, mode) + time.sleep(5) def is_temp_file(self, filename): info = filename.split(".") if(info[-2] in self.supported_file_formats): return True - else : + else: return False def is_audio_file(self, filename): @@ -245,63 +326,124 @@ class MediaMonitor(ProcessEvent): if(info[-1] in self.supported_file_formats): return True - else : + else: return False - def process_IN_CREATE(self, event): if not event.dir: - + self.logger.info("%s: %s", event.maskname, event.pathname) #file created is a tmp file which will be modified and then moved back to the original filename. if self.is_temp_file(event.name) : self.temp_files[event.pathname] = None #This is a newly imported file. else : - #if not is_renamed_file(event.pathname): - self.create_file_path(event.pathname) + global plupload_directory + #files that have been added through plupload have a placeholder already put in Airtime's database. + if not self.is_parent_directory(event.pathname, plupload_directory): + md5 = self.get_md5(event.pathname) + response = self.api_client.check_media_status(md5) - self.logger.info("%s: %s", event.maskname, event.pathname) + #this file is new, md5 does not exist in Airtime. + if(response['airtime_status'] == 0): + filepath = self.create_file_path(event.pathname) + os.rename(event.pathname, filepath) + self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath}) def process_IN_MODIFY(self, event): - if not event.dir : - - if self.is_audio_file(event.name) : - self.update_airtime(event) - - self.logger.info("%s: %s", event.maskname, event.pathname) + if not event.dir: + self.logger.info("%s: %s", event.maskname, event.pathname) + global plupload_directory + #files that have been added through plupload have a placeholder already put in Airtime's database. + if not self.is_parent_directory(event.pathname, plupload_directory): + if self.is_audio_file(event.name) : + self.file_events.append({'filepath': event.pathname, 'mode': MODE_MODIFY}) def process_IN_MOVED_FROM(self, event): - if event.pathname in self.temp_files : + self.logger.info("%s: %s", event.maskname, event.pathname) + 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) + else: + self.moved_files[event.cookie] = 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) + if event.cookie in self.temp_files: + del self.temp_files[event.cookie] + self.file_events.append({'filepath': event.pathname, 'mode': MODE_MODIFY}) + elif event.cookie in self.moved_files: + old_filepath = self.moved_files[event.cookie] + del self.moved_files[event.cookie] + + global plupload_directory + if self.is_parent_directory(old_filepath, plupload_directory): + #file renamed from /tmp/plupload does not have a path in our naming scheme yet. + md_filepath = self.create_file_path(event.pathname) + #move the file a second time to its correct Airtime naming schema. + os.rename(event.pathname, md_filepath) + self.file_events.append({'filepath': md_filepath, 'mode': MODE_MOVED}) + else: + self.file_events.append({'filepath': event.pathname, 'mode': MODE_MOVED}) + + else: + #TODO need to pass in if md5 exists to this file creation function, identical files will just replace current files not have a (1) etc. + #file has been most likely dropped into stor folder from an unwatched location. (from gui, mv command not cp) + md_filepath = self.create_file_path(event.pathname) + os.rename(event.pathname, md_filepath) + self.file_events.append({'mode': MODE_CREATE, 'filepath': md_filepath}) + + def process_IN_DELETE(self, event): + if not event.dir: + self.logger.info("%s: %s", event.maskname, event.pathname) + self.file_events.append({'filepath': event.pathname, 'mode': MODE_DELETE}) def process_default(self, event): self.logger.info("%s: %s", event.maskname, event.pathname) + def notifier_loop_callback(self, notifier): + + while len(self.file_events) > 0: + file_info = self.file_events.popleft() + self.update_airtime(file_info) + + try: + notifier.connection.drain_events(timeout=1) + except Exception, e: + self.logger.info("%s", e) + if __name__ == '__main__': try: - # 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(storage_directory, mask, rec=True, auto_add=True) - logger = logging.getLogger('root') - logger.info("Added watch to %s", storage_directory) + mm = MediaMonitor() - notifier = AirtimeNotifier(wm, MediaMonitor(), read_freq=int(config["check_filesystem_events"]), timeout=1) + response = None + while response is None: + response = mm.api_client.setup_media_monitor() + time.sleep(5) + + storage_directory = response["stor"] + plupload_directory = response["plupload"] + + wdd = mm.watch_directory(storage_directory) + logger.info("Added watch to %s", storage_directory) + logger.info("wdd result %s", wdd[storage_directory]) + wdd = mm.watch_directory(plupload_directory) + logger.info("Added watch to %s", plupload_directory) + logger.info("wdd result %s", wdd[plupload_directory]) + + notifier = AirtimeNotifier(mm.wm, mm, read_freq=int(config["check_filesystem_events"]), timeout=1) notifier.coalesce_events() - notifier.loop(callback=checkRabbitMQ) + + #notifier.loop(callback=mm.notifier_loop_callback) + + while True: + if(notifier.check_events(1)): + notifier.read_events() + notifier.process_events() + mm.notifier_loop_callback(notifier) + except KeyboardInterrupt: notifier.stop() + except Exception, e: + logger.error('Exception: %s', e) diff --git a/python_apps/media-monitor/media-monitor.cfg b/python_apps/media-monitor/media-monitor.cfg index 38b44cea7..e1f36619f 100644 --- a/python_apps/media-monitor/media-monitor.cfg +++ b/python_apps/media-monitor/media-monitor.cfg @@ -19,8 +19,14 @@ api_base = 'api' # URL to get the version number of the server API version_url = 'version/api_key/%%api_key%%' +# URL to setup the media monitor +media_setup_url = 'media-monitor-setup/format/json/api_key/%%api_key%%' + +# URL to check Airtime's status of a file +media_status_url = 'media-item-status/format/json/api_key/%%api_key%%/md5/%%md5%%' + # URL to tell Airtime to update file's meta data -update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%' +update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%/mode/%%mode%%' ############################################ # RabbitMQ settings #