diff --git a/CREDITS b/CREDITS index d0dc998a5..b6108f552 100644 --- a/CREDITS +++ b/CREDITS @@ -2,6 +2,10 @@ CREDITS ======= +Version 1.9.0 +------------- +Same as previous version. + Version 1.8.2 ------------- Welcome to James Moon! diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index c6d7dfc13..af6aafd4f 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -54,7 +54,7 @@ class ApiController extends Zend_Controller_Action * Allows remote client to download requested media file. * * @return void - * The given value increased by the increment amount. + * */ public function getMediaAction() { @@ -65,7 +65,7 @@ class ApiController extends Zend_Controller_Action $this->_helper->viewRenderer->setNoRender(true); $api_key = $this->_getParam('api_key'); - $downlaod = $this->_getParam('download'); + $download = ("true" == $this->_getParam('download')); if(!in_array($api_key, $CC_CONFIG["apiKey"])) { @@ -87,7 +87,6 @@ class ApiController extends Zend_Controller_Action exit; } - // possibly use fileinfo module here in the future. // http://www.php.net/manual/en/book.fileinfo.php $ext = pathinfo($filename, PATHINFO_EXTENSION); @@ -96,7 +95,12 @@ class ApiController extends Zend_Controller_Action else if ($ext == "mp3") header("Content-Type: audio/mpeg"); if ($download){ - header('Content-Disposition: attachment; filename="'.$media->getName().'"'); + //path_info breaks up a file path into seperate pieces of informaiton. + //We just want the basename which is the file name with the path + //information stripped away. We are using Content-Disposition to specify + //to the browser what name the file should be saved as. + $path_parts = pathinfo($media->getPropelOrm()->getDbFilepath()); + header('Content-Disposition: attachment; filename="'.$path_parts['basename'].'"'); } header("Content-Length: " . filesize($filepath)); @@ -408,7 +412,8 @@ class ApiController extends Zend_Controller_Action public function reloadMetadataAction() { global $CC_CONFIG; - $api_key = $this->_getParam('api_key'); + $request = $this->getRequest(); + $api_key = $request->getParam('api_key'); if (!in_array($api_key, $CC_CONFIG["apiKey"])) { header('HTTP/1.0 401 Unauthorized'); @@ -416,8 +421,16 @@ class ApiController extends Zend_Controller_Action exit; } - $md = $this->_getParam('md'); - $mode = $this->_getParam('mode'); + $mode = $request->getParam('mode'); + $params = $request->getParams(); + + $md = array(); + //extract all file metadata params from the request. + foreach ($params as $key => $value) { + if (preg_match('/^MDATA_KEY/', $key)) { + $md[$key] = $value; + } + } if ($mode == "create") { $md5 = $md['MDATA_KEY_MD5']; diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 6c35f491c..04cddcd08 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -28,6 +28,7 @@ class LibraryController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'/js/jplayer/jquery.jplayer.min.js'); $this->view->headScript()->appendFile($baseUrl.'/js/datatables/js/jquery.dataTables.js','text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.pluginAPI.js','text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.fnSetFilteringDelay.js','text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/library.js','text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/advancedsearch.js','text/javascript'); @@ -166,7 +167,7 @@ class LibraryController extends Zend_Controller_Action $data = $file->getMetadata(); - RabbitMq::SendFileMetaData($data); + RabbitMq::SendMessageToMediaMonitor("md_update", $data); $this->_helper->redirector('index'); } diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index a3ece83ab..ec2cc345b 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -113,8 +113,8 @@ class PlaylistController extends Zend_Controller_Action $this->changePlaylist($pl_id); $pl = $this->getPlaylist(); - $title = $pl->getPLMetaData(UI_MDATA_KEY_TITLE); - $desc = $pl->getPLMetaData(UI_MDATA_KEY_DESCRIPTION); + $title = $pl->getPLMetaData("dc:title"); + $desc = $pl->getPLMetaData("dc:description"); $data = array( 'title' => $title, 'description' => $desc); $form->populate($data); @@ -130,7 +130,7 @@ class PlaylistController extends Zend_Controller_Action $pl->setName($title); if(isset($description)) { - $pl->setPLMetaData(UI_MDATA_KEY_DESCRIPTION, $description); + $pl->setPLMetaData("dc:description", $description); } $this->view->pl = $pl; diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 82b1a1ff3..51882fbcc 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -19,23 +19,24 @@ class PreferenceController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'/js/airtime/preferences/preferences.js','text/javascript'); $this->view->statusMsg = ""; - + $form = new Application_Form_Preferences(); - + if ($request->isPost()) { - + if ($form->isValid($request->getPost())) { $values = $form->getValues(); - - Application_Model_Preference::SetHeadTitle($values["preferences_general"]["stationName"], $this->view); - Application_Model_Preference::SetDefaultFade($values["preferences_general"]["stationDefaultFade"]); + + Application_Model_Preference::SetHeadTitle($values["preferences_general"]["stationName"], $this->view); + Application_Model_Preference::SetDefaultFade($values["preferences_general"]["stationDefaultFade"]); Application_Model_Preference::SetStreamLabelFormat($values["preferences_general"]["streamFormat"]); Application_Model_Preference::SetAllow3rdPartyApi($values["preferences_general"]["thirdPartyApi"]); + Application_Model_Preference::SetWatchedDirectory($values["preferences_general"]["watchedFolder"]); - Application_Model_Preference::SetDoSoundCloudUpload($values["preferences_soundcloud"]["UseSoundCloud"]); + Application_Model_Preference::SetDoSoundCloudUpload($values["preferences_soundcloud"]["UseSoundCloud"]); Application_Model_Preference::SetSoundCloudUser($values["preferences_soundcloud"]["SoundCloudUser"]); - Application_Model_Preference::SetSoundCloudPassword($values["preferences_soundcloud"]["SoundCloudPassword"]); + Application_Model_Preference::SetSoundCloudPassword($values["preferences_soundcloud"]["SoundCloudPassword"]); Application_Model_Preference::SetSoundCloudTags($values["preferences_soundcloud"]["SoundCloudTags"]); Application_Model_Preference::SetSoundCloudGenre($values["preferences_soundcloud"]["SoundCloudGenre"]); Application_Model_Preference::SetSoundCloudTrackType($values["preferences_soundcloud"]["SoundCloudTrackType"]); @@ -54,11 +55,13 @@ class PreferenceController extends Zend_Controller_Action Application_Model_Preference::SetStationDescription($values["preferences_support"]["Description"]); Application_Model_Preference::SetStationLogo($imagePath); + $data = array(); + $data["directory"] = $values["preferences_general"]["watchedFolder"]; + RabbitMq::SendMessageToMediaMonitor("new_watch", $data); $this->view->statusMsg = "
Preferences updated.
"; } } - $this->view->supportFeedback = Application_Model_Preference::GetSupportFeedback(); $logo = Application_Model_Preference::GetStationLogo(); if($logo){ diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php index 0e5b55590..ffac3e911 100644 --- a/airtime_mvc/application/forms/GeneralPreferences.php +++ b/airtime_mvc/application/forms/GeneralPreferences.php @@ -33,15 +33,15 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm 'label' => 'Default Fade:', 'required' => false, 'filters' => array('StringTrim'), - 'validators' => array(array('regex', false, - array('/^[0-2][0-3]:[0-5][0-9]:[0-5][0-9](\.\d{1,6})?$/', + 'validators' => array(array('regex', false, + array('/^[0-2][0-3]:[0-5][0-9]:[0-5][0-9](\.\d{1,6})?$/', 'messages' => 'enter a time 00:00:00{.000000}'))), 'value' => $defaultFade, 'decorators' => array( 'ViewHelper' ) )); - + $stream_format = new Zend_Form_Element_Radio('streamFormat'); $stream_format->setLabel('Stream Label:'); $stream_format->setMultiOptions(array("Artist - Title", @@ -58,6 +58,18 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm $third_party_api->setValue(Application_Model_Preference::GetAllow3rdPartyApi()); $third_party_api->setDecorators(array('ViewHelper')); $this->addElement($third_party_api); + + //Default station fade + $this->addElement('text', 'watchedFolder', array( + 'class' => 'input_text', + 'label' => 'WatchedFolder:', + 'required' => false, + 'filters' => array('StringTrim'), + 'value' => Application_Model_Preference::GetWatchedDirectory(), + 'decorators' => array( + 'ViewHelper' + ) + )); } diff --git a/airtime_mvc/application/models/DateHelper.php b/airtime_mvc/application/models/DateHelper.php index d8f44a3c5..bc7b21a86 100644 --- a/airtime_mvc/application/models/DateHelper.php +++ b/airtime_mvc/application/models/DateHelper.php @@ -127,5 +127,35 @@ class DateHelper $explode = explode(" ", $p_timestamp); return $explode[1]; } + + /* Given a track length in the format HH:MM:SS.mm, we want to + * convert this to seconds. This is useful for Liquidsoap which + * likes input parameters give in seconds. + * For example, 00:06:31.444, should be converted to 391.444 seconds + * @param int $p_time + * The time interval in format HH:MM:SS.mm we wish to + * convert to seconds. + * @return int + * The input parameter converted to seconds. + */ + public static function calculateLengthInSeconds($p_time){ + + if (2 !== substr_count($p_time, ":")){ + return FALSE; + } + + if (1 === substr_count($p_time, ".")){ + list($hhmmss, $ms) = explode(".", $p_time); + } else { + $hhmmss = $p_time; + $ms = 0; + } + + list($hours, $minutes, $seconds) = explode(":", $hhmmss); + + $totalSeconds = $hours*3600 + $minutes*60 + $seconds + $ms/1000; + + return $totalSeconds; + } } diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php index fd4c3cead..e383708cd 100644 --- a/airtime_mvc/application/models/Playlist.php +++ b/airtime_mvc/application/models/Playlist.php @@ -393,7 +393,7 @@ class Playlist { ->orderByDbPosition() ->filterByDbPlaylistId($this->id) ->find(); - + $i = 0; $offset = 0; foreach ($rows as $row) { @@ -502,7 +502,7 @@ class Playlist { } $metadata = $media->getMetadata(); - $length = $metadata["dcterms:extent"]; + $length = $metadata['MDATA_KEY_DURATION']; if (!is_null($p_clipLength)) { $length = $p_clipLength; diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index e92ffbe48..082406fd8 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -36,7 +36,7 @@ class Application_Model_Preference else if(is_null($id)) { $sql = "INSERT INTO cc_pref (keystr, valstr)" ." VALUES ('$key', '$value')"; - } + } else { $sql = "INSERT INTO cc_pref (subjid, keystr, valstr)" ." VALUES ($id, '$key', '$value')"; @@ -188,6 +188,7 @@ class Application_Model_Preference return $val; } } +<<<<<<< HEAD public static function SetPhone($phone){ Application_Model_Preference::SetValue("phone", $phone); @@ -350,5 +351,16 @@ class Application_Model_Preference return Application_Model_Preference::GetValue("remindme"); } +======= + + public static function SetWatchedDirectory($directory) { + Application_Model_Preference::SetValue("watched_directory", $directory); + } + + public static function GetWatchedDirectory() { + return Application_Model_Preference::GetValue("watched_directory"); + } + +>>>>>>> 898cdc64dc65c03d2ed6e3f3344b273df7c0d201 } diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php index ee70ae6c5..0cd92bd13 100644 --- a/airtime_mvc/application/models/RabbitMq.php +++ b/airtime_mvc/application/models/RabbitMq.php @@ -40,10 +40,12 @@ class RabbitMq } } - public static function SendFileMetaData($md) + public static function SendMessageToMediaMonitor($event_type, $md) { global $CC_CONFIG; + $md["event_type"] = $event_type; + $conn = new AMQPConnection($CC_CONFIG["rabbitmq"]["host"], $CC_CONFIG["rabbitmq"]["port"], $CC_CONFIG["rabbitmq"]["user"], diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 0bf61f4d4..713ca2344 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -184,7 +184,9 @@ class ScheduleGroup { ." st.cue_out," ." st.clip_length," ." st.fade_in," - ." st.fade_out" + ." st.fade_out," + ." st.starts," + ." st.ends" ." FROM $CC_CONFIG[scheduleTable] as st" ." LEFT JOIN $CC_CONFIG[showInstances] as si" ." ON st.instance_id = si.id" @@ -676,7 +678,7 @@ class Schedule { $timestamp = strtotime($start); $playlists[$pkey]['source'] = "PLAYLIST"; $playlists[$pkey]['x_ident'] = $dx['group_id']; - $playlists[$pkey]['subtype'] = '1'; // Just needs to be between 1 and 4 inclusive + //$playlists[$pkey]['subtype'] = '1'; // Just needs to be between 1 and 4 inclusive $playlists[$pkey]['timestamp'] = $timestamp; $playlists[$pkey]['duration'] = $dx['clip_length']; $playlists[$pkey]['played'] = '0'; @@ -696,27 +698,24 @@ class Schedule { $scheduleGroup = new ScheduleGroup($playlist["schedule_id"]); $items = $scheduleGroup->getItems(); $medias = array(); - $playlist['subtype'] = '1'; foreach ($items as $item) { $storedFile = StoredFile::Recall($item["file_id"]); $uri = $storedFile->getFileUrl(); - // For pypo, a cueout of zero means no cueout - $cueOut = "0"; - if (Schedule::TimeDiff($item["cue_out"], $item["clip_length"]) > 0.001) { - $cueOut = Schedule::WallTimeToMillisecs($item["cue_out"]); - } - $medias[] = array( + $starts = Schedule::AirtimeTimeToPypoTime($item["starts"]); + $medias[$starts] = array( 'row_id' => $item["id"], 'id' => $storedFile->getGunid(), 'uri' => $uri, 'fade_in' => Schedule::WallTimeToMillisecs($item["fade_in"]), 'fade_out' => Schedule::WallTimeToMillisecs($item["fade_out"]), 'fade_cross' => 0, - 'cue_in' => Schedule::WallTimeToMillisecs($item["cue_in"]), - 'cue_out' => $cueOut, - 'export_source' => 'scheduler' + 'cue_in' => DateHelper::CalculateLengthInSeconds($item["cue_in"]), + 'cue_out' => DateHelper::CalculateLengthInSeconds($item["cue_out"]), + 'export_source' => 'scheduler', + 'start' => $starts, + 'end' => Schedule::AirtimeTimeToPypoTime($item["ends"]) ); } $playlist['medias'] = $medias; diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index c9ff830c7..04a8cc62a 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -63,6 +63,10 @@ class StoredFile { return $this->_file->getDbFtype(); } + public function getPropelOrm(){ + return $this->_file; + } + public function setFormat($p_format) { $this->_file->setDbFtype($p_format); diff --git a/airtime_mvc/application/views/scripts/form/preferences_general.phtml b/airtime_mvc/application/views/scripts/form/preferences_general.phtml index aa4e5d587..0f4ca0743 100644 --- a/airtime_mvc/application/views/scripts/form/preferences_general.phtml +++ b/airtime_mvc/application/views/scripts/form/preferences_general.phtml @@ -73,6 +73,19 @@ +
+ +
+
+ element->getElement('watchedFolder') ?> + element->getElement('watchedFolder')->hasErrors()) : ?> + + +
diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index e40e25fa7..fdb95eb1f 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -175,5 +175,5 @@ $(document).ready(function() { "oLanguage": { "sSearch": "" } - }); + }).fnSetFilteringDelay(350); }); diff --git a/airtime_mvc/public/js/datatables/plugin/dataTables.fnSetFilteringDelay.js b/airtime_mvc/public/js/datatables/plugin/dataTables.fnSetFilteringDelay.js new file mode 100644 index 000000000..43da95055 --- /dev/null +++ b/airtime_mvc/public/js/datatables/plugin/dataTables.fnSetFilteringDelay.js @@ -0,0 +1,38 @@ +jQuery.fn.dataTableExt.oApi.fnSetFilteringDelay = function ( oSettings, iDelay ) { + /* + * Inputs: object:oSettings - dataTables settings object - automatically given + * integer:iDelay - delay in milliseconds + * Usage: $('#example').dataTable().fnSetFilteringDelay(250); + * Author: Zygimantas Berziunas (www.zygimantas.com) and Allan Jardine + * License: GPL v2 or BSD 3 point style + * Contact: zygimantas.berziunas /AT\ hotmail.com + */ + var + _that = this, + iDelay = (typeof iDelay == 'undefined') ? 250 : iDelay; + + this.each( function ( i ) { + $.fn.dataTableExt.iApiIndex = i; + var + $this = this, + oTimerId = null, + sPreviousSearch = null, + anControl = $( 'input', _that.fnSettings().aanFeatures.f ); + + anControl.unbind( 'keyup' ).bind( 'keyup', function() { + var $$this = $this; + + if (sPreviousSearch === null || sPreviousSearch != anControl.val()) { + window.clearTimeout(oTimerId); + sPreviousSearch = anControl.val(); + oTimerId = window.setTimeout(function() { + $.fn.dataTableExt.iApiIndex = i; + _that.fnFilter( anControl.val() ); + }, iDelay); + } + }); + + return this; + } ); + return this; +} diff --git a/install/airtime-install b/install/airtime-install index bc2187d68..f746e22b5 100755 --- a/install/airtime-install +++ b/install/airtime-install @@ -14,7 +14,7 @@ echo -e "\n******************************** Install Begin ********************** echo -e "\n*** Creating Pypo User ***" python ${SCRIPTPATH}/../python_apps/create-pypo-user.py -php ${SCRIPTPATH}/airtime-install.php $@ +php ${SCRIPTPATH}/include/airtime-install.php $@ echo -e "\n*** Pypo Installation ***" python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-install.py @@ -26,6 +26,7 @@ echo -e "\n*** Media Monitor Installation ***" python ${SCRIPTPATH}/../python_apps/media-monitor/install/media-monitor-install.py sleep 4 +echo -e "\n*** Verifying your system environment ***" airtime-check-system echo -e "\n******************************* Install Complete *******************************" diff --git a/install/airtime-uninstall b/install/airtime-uninstall index b65700d00..9f708e10f 100755 --- a/install/airtime-uninstall +++ b/install/airtime-uninstall @@ -1,5 +1,9 @@ #!/bin/bash +#Cause bash script to exit if any of the installers +#return with a non-zero return value. +set -e + # Absolute path to this script SCRIPT=`readlink -f $0` # Absolute directory this script is in @@ -7,8 +11,6 @@ SCRIPTPATH=`dirname $SCRIPT` echo -e "\n******************************* Uninstall Begin ********************************" -php ${SCRIPTPATH}/airtime-uninstall.php - echo -e "\n*** Uninstalling Pypo ***" python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-uninstall.py @@ -21,6 +23,9 @@ python ${SCRIPTPATH}/../python_apps/media-monitor/install/media-monitor-uninstal echo -e "\n*** Removing Pypo User ***" python ${SCRIPTPATH}/../python_apps/remove-pypo-user.py +php ${SCRIPTPATH}/include/airtime-uninstall.php + + echo -e "\n****************************** Uninstall Complete ******************************\n" echo "NOTE: To fully remove all Airtime files, you will also have to manually delete" echo " the directories '/srv/airtime'(default storage location of media files)" diff --git a/install/include/AirtimeIni.php b/install/include/AirtimeIni.php index dd228f997..88ed2c677 100644 --- a/install/include/AirtimeIni.php +++ b/install/include/AirtimeIni.php @@ -27,6 +27,7 @@ class AirtimeIni const CONF_FILE_RECORDER = "/etc/airtime/recorder.cfg"; const CONF_FILE_LIQUIDSOAP = "/etc/airtime/liquidsoap.cfg"; const CONF_FILE_MEDIAMONITOR = "/etc/airtime/media-monitor.cfg"; + const CONF_FILE_MONIT = "/etc/monit/conf.d/airtime-monit.cfg"; public static function IniFilesExist() { @@ -75,10 +76,21 @@ class AirtimeIni exit(1); } if (!copy(__DIR__."/../../python_apps/media-monitor/media-monitor.cfg", AirtimeIni::CONF_FILE_MEDIAMONITOR)){ - echo "Could not copy MediaMonitor.cfg to /etc/airtime/. Exiting."; + echo "Could not copy media-monitor.cfg to /etc/airtime/. Exiting."; exit(1); } } + + public static function CreateMonitFile(){ + if (!copy(__DIR__."/../../python_apps/monit/airtime-monit.cfg", AirtimeIni::CONF_FILE_MONIT)){ + echo "Could not copy airtime-monit.cfg to /etc/monit/conf.d/. Exiting."; + exit(1); + } + } + + public static function RemoveMonitFile(){ + @unlink("/etc/monit/conf.d/airtime-monit.cfg"); + } /** * This function removes /etc/airtime and the configuration @@ -187,7 +199,6 @@ class AirtimeIni 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); } public static function ReadPythonConfig($p_filename) diff --git a/install/include/AirtimeInstall.php b/install/include/AirtimeInstall.php index 65c34046c..fe2d0c618 100644 --- a/install/include/AirtimeInstall.php +++ b/install/include/AirtimeInstall.php @@ -290,7 +290,7 @@ class AirtimeInstall public static function DeleteFilesRecursive($p_path) { - $command = "rm -rf $p_path"; + $command = "rm -rf \"$p_path\""; exec($command); } @@ -336,7 +336,7 @@ class AirtimeInstall public static function UninstallPhpCode() { echo "* Removing PHP code from ".AirtimeInstall::CONF_DIR_WWW.PHP_EOL; - exec("rm -rf ".AirtimeInstall::CONF_DIR_WWW); + exec('rm -rf "'.AirtimeInstall::CONF_DIR_WWW.'"'); } public static function InstallBinaries() @@ -349,7 +349,7 @@ class AirtimeInstall public static function UninstallBinaries() { echo "* Removing Airtime binaries from ".AirtimeInstall::CONF_DIR_BINARIES.PHP_EOL; - exec("rm -rf ".AirtimeInstall::CONF_DIR_BINARIES); + exec('rm -rf "'.AirtimeInstall::CONF_DIR_BINARIES.'"'); } public static function DirCheck() @@ -399,6 +399,6 @@ class AirtimeInstall $path = AirtimeInstall::CONF_DIR_LOG; echo "* Removing logs directory ".$path.PHP_EOL; - exec("rm -rf $path"); + exec("rm -rf \"$path\""); } } diff --git a/install/airtime-install.php b/install/include/airtime-install.php similarity index 89% rename from install/airtime-install.php rename to install/include/airtime-install.php index ac44d4715..a6d877abd 100644 --- a/install/airtime-install.php +++ b/install/include/airtime-install.php @@ -8,10 +8,10 @@ * Performs a new install (new configs, database install) if a version of Airtime is not found * If the current version is found to be installed the User is presented with the help menu and can choose -r to reinstall. */ -set_include_path(__DIR__.'/../airtime_mvc/library' . PATH_SEPARATOR . get_include_path()); +set_include_path(__DIR__.'/../../airtime_mvc/library' . PATH_SEPARATOR . get_include_path()); -require_once(dirname(__FILE__).'/include/AirtimeIni.php'); -require_once(dirname(__FILE__).'/include/AirtimeInstall.php'); +require_once(dirname(__FILE__).'/AirtimeIni.php'); +require_once(dirname(__FILE__).'/AirtimeInstall.php'); require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/constants.php'); AirtimeInstall::ExitIfNotRoot(); @@ -97,6 +97,8 @@ if ($overwrite) { echo "* Creating INI files".PHP_EOL; AirtimeIni::CreateIniFiles(); } +AirtimeIni::CreateMonitFile(); + AirtimeInstall::InstallPhpCode(); AirtimeInstall::InstallBinaries(); @@ -106,6 +108,9 @@ if ($overwrite) { AirtimeIni::UpdateIniFiles(); } +// Update the build.properties file to point to the correct directory. +AirtimeIni::UpdateIniValue(AirtimeInstall::CONF_DIR_WWW.'/build/build.properties', 'project.home', AirtimeInstall::CONF_DIR_WWW); + require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/conf.php'); echo "* Airtime Version: ".AIRTIME_VERSION.PHP_EOL; diff --git a/install/airtime-uninstall.php b/install/include/airtime-uninstall.php similarity index 94% rename from install/airtime-uninstall.php rename to install/include/airtime-uninstall.php index a5c7dc304..da2a58ed3 100644 --- a/install/airtime-uninstall.php +++ b/install/include/airtime-uninstall.php @@ -5,8 +5,8 @@ * @license http://www.gnu.org/licenses/gpl.txt */ -require_once(dirname(__FILE__).'/include/AirtimeIni.php'); -require_once(dirname(__FILE__).'/include/AirtimeInstall.php'); +require_once(dirname(__FILE__).'/AirtimeIni.php'); +require_once(dirname(__FILE__).'/AirtimeInstall.php'); // Need to check that we are superuser before running this. AirtimeInstall::ExitIfNotRoot(); @@ -69,7 +69,7 @@ if ($dbDeleteFailed) { // Delete the user //------------------------------------------------------------------------ echo " * Deleting database user '{$CC_CONFIG['dsn']['username']}'...".PHP_EOL; -$command = "echo \"DROP USER IF EXISTS {$CC_CONFIG['dsn']['username']}\" | su postgres -c psql"; +$command = "echo \"DROP USER IF EXISTS {$CC_CONFIG['dsn']['username']}\" | su postgres -c psql >/dev/null 2>&1"; @exec($command, $output, $results); if ($results == 0) { echo " * User '{$CC_CONFIG['dsn']['username']}' deleted.".PHP_EOL; @@ -88,6 +88,7 @@ if ($results == 0) { AirtimeInstall::RemoveSymlinks(); AirtimeInstall::UninstallBinaries(); AirtimeInstall::RemoveLogDirectories(); +AirtimeIni::RemoveMonitFile(); unlink('/etc/cron.d/airtime-crons'); diff --git a/install/airtime-upgrade.php b/install/include/airtime-upgrade.php similarity index 73% rename from install/airtime-upgrade.php rename to install/include/airtime-upgrade.php index 109a978ec..4a242a518 100644 --- a/install/airtime-upgrade.php +++ b/install/include/airtime-upgrade.php @@ -7,8 +7,9 @@ */ //Pear classes. -set_include_path(__DIR__.'/../airtime_mvc/library/pear' . PATH_SEPARATOR . get_include_path()); +set_include_path(__DIR__.'/../../airtime_mvc/library/pear' . PATH_SEPARATOR . get_include_path()); require_once('DB.php'); +require_once(dirname(__FILE__).'/AirtimeIni.php'); if(exec("whoami") != "root"){ echo "Must be root user.\n"; @@ -67,19 +68,19 @@ echo "******************************** Update Begin **************************** $version = substr($version, 0, 5); if (strcmp($version, "1.7.0") < 0){ - system("php ".__DIR__."/upgrades/airtime-1.7/airtime-upgrade.php"); + system("php ".__DIR__."/../upgrades/airtime-1.7/airtime-upgrade.php"); } if (strcmp($version, "1.8.0") < 0){ - system("php ".__DIR__."/upgrades/airtime-1.8/airtime-upgrade.php"); + system("php ".__DIR__."/../upgrades/airtime-1.8/airtime-upgrade.php"); } if (strcmp($version, "1.8.1") < 0){ - system("php ".__DIR__."/upgrades/airtime-1.8.1/airtime-upgrade.php"); + system("php ".__DIR__."/../upgrades/airtime-1.8.1/airtime-upgrade.php"); } if (strcmp($version, "1.8.2") < 0){ - system("php ".__DIR__."/upgrades/airtime-1.8.2/airtime-upgrade.php"); + system("php ".__DIR__."/../upgrades/airtime-1.8.2/airtime-upgrade.php"); } if (strcmp($version, "1.9.0") < 0){ - system("php ".__DIR__."/upgrades/airtime-1.9/airtime-upgrade.php"); + system("php ".__DIR__."/../upgrades/airtime-1.9/airtime-upgrade.php"); } @@ -91,13 +92,15 @@ $CC_DBC->query($sql); echo PHP_EOL."*** Updating Pypo ***".PHP_EOL; -passthru("python ".__DIR__."/../python_apps/pypo/install/pypo-install.py"); +passthru("python ".__DIR__."/../../python_apps/pypo/install/pypo-install.py"); echo PHP_EOL."*** Updating Recorder ***".PHP_EOL; -passthru("python ".__DIR__."/../python_apps/show-recorder/install/recorder-install.py"); +passthru("python ".__DIR__."/../../python_apps/show-recorder/install/recorder-install.py"); -echo PHP_EOL."*** Starting Media Monitor ***".PHP_EOL; -passthru("python ".__DIR__."/../python_apps/media-monitor/install/media-monitor-install.py"); +echo PHP_EOL."*** Updating Media Monitor ***".PHP_EOL; +passthru("python ".__DIR__."/../../python_apps/media-monitor/install/media-monitor-install.py"); + +AirtimeIni::CreateMonitFile(); echo "******************************* Update Complete *******************************".PHP_EOL; diff --git a/install/upgrades/airtime-1.9/airtime-upgrade.php b/install/upgrades/airtime-1.9/airtime-upgrade.php index 0727c9c64..51ab6091b 100644 --- a/install/upgrades/airtime-1.9/airtime-upgrade.php +++ b/install/upgrades/airtime-1.9/airtime-upgrade.php @@ -39,7 +39,7 @@ function InstallBinaries() function UninstallBinaries() { echo "* Removing Airtime binaries from ".CONF_DIR_BINARIES.PHP_EOL; - exec("rm -rf ".CONF_DIR_BINARIES); + exec('rm -rf "'.CONF_DIR_BINARIES.'"'); } @@ -74,7 +74,7 @@ $pathnames = array("/usr/bin/airtime-pypo-start", foreach ($pathnames as $pn){ echo "Removing $pn\n"; - exec("rm -rf ".$pn); + exec("rm -rf \"$pn\""); } diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index 016041255..abf58917c 100644 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -31,24 +31,6 @@ def api_client_factory(config): logger.info('API Client "'+config["api_client"]+'" not supported. Please check your config file.\n') 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 @@ -402,11 +384,12 @@ 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["update_media_url"]) - logger.debug(url) + url = url.replace("%%api_key%%", self.config["api_key"]) url = url.replace("%%mode%%", mode) + logger.debug(url) - data = recursive_urlencode(md) + data = urllib.urlencode(md) req = urllib2.Request(url, data) response = urllib2.urlopen(req).read() @@ -636,7 +619,7 @@ class ObpApiClient(): def get_liquidsoap_data(self, pkey, schedule): playlist = schedule[pkey] data = dict() - data["ptype"] = playlist['subtype'] + #data["ptype"] = playlist['subtype'] try: data["user_id"] = playlist['user_id'] data["playlist_id"] = playlist['id'] diff --git a/python_apps/create-pypo-user.py b/python_apps/create-pypo-user.py index 38619a0f3..46a7bb1b9 100644 --- a/python_apps/create-pypo-user.py +++ b/python_apps/create-pypo-user.py @@ -1,4 +1,5 @@ import os +import sys from subprocess import Popen, PIPE, STDOUT def create_user(username): diff --git a/python_apps/media-monitor/MediaMonitor.py b/python_apps/media-monitor/MediaMonitor.py index 53f891069..d30091772 100644 --- a/python_apps/media-monitor/MediaMonitor.py +++ b/python_apps/media-monitor/MediaMonitor.py @@ -10,9 +10,12 @@ import hashlib import json import shutil import math +import socket +import grp +import pwd from collections import deque -from pwd import getpwnam + from subprocess import Popen, PIPE, STDOUT from configobj import ConfigObj @@ -26,6 +29,8 @@ from kombu.connection import BrokerConnection from kombu.messaging import Exchange, Queue, Consumer, Producer from api_clients import api_client +from multiprocessing import Process, Lock + MODE_CREATE = "create" MODE_MODIFY = "modify" MODE_MOVED = "moved" @@ -54,10 +59,9 @@ 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'] """ -class AirtimeNotifier(Notifier): +class MetadataExtractor: - 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) + def __init__(self): self.airtime2mutagen = {\ "MDATA_KEY_TITLE": "title",\ @@ -77,50 +81,6 @@ class AirtimeNotifier(Notifier): "MDATA_KEY_COPYRIGHT": "copyright",\ } - 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() - - self.logger = logging.getLogger('root') - - def handle_message(self, body, message): - # ACK the message to take it off the queue - message.ack() - - self.logger.info("Received md from RabbitMQ: " + body) - - try: - m = json.loads(message.body) - airtime_file = mutagen.File(m['MDATA_KEY_FILEPATH'], easy=True) - - for key in m.keys() : - if key in self.airtime2mutagen: - value = m[key] - if ((value is not None) and (len(str(value)) > 0)): - airtime_file[self.airtime2mutagen[key]] = str(value) - self.logger.info('setting %s = %s ', key, str(value)) - - - airtime_file.save() - except Exception, e: - self.logger.error('Trying to save md') - self.logger.error('Exception: %s', e) - self.logger.error('Filepath %s', m['MDATA_KEY_FILEPATH']) - -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": "MDATA_KEY_TITLE",\ "artist": "MDATA_KEY_CREATOR",\ @@ -139,26 +99,7 @@ class MediaMonitor(ProcessEvent): "copyright": "MDATA_KEY_COPYRIGHT",\ } - self.supported_file_formats = ['mp3', 'ogg'] self.logger = logging.getLogger('root') - self.temp_files = {} - self.moved_files = {} - self.file_events = deque() - - self.mask = pyinotify.ALL_EVENTS - - 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') @@ -185,6 +126,192 @@ class MediaMonitor(ProcessEvent): return length + def save_md_to_file(self, m): + try: + airtime_file = mutagen.File(m['MDATA_KEY_FILEPATH'], easy=True) + + for key in m.keys() : + if key in self.airtime2mutagen: + value = m[key] + if ((value is not None) and (len(str(value)) > 0)): + airtime_file[self.airtime2mutagen[key]] = str(value) + #self.logger.info('setting %s = %s ', key, str(value)) + + + airtime_file.save() + except Exception, e: + self.logger.error('Trying to save md') + self.logger.error('Exception: %s', e) + self.logger.error('Filepath %s', m['MDATA_KEY_FILEPATH']) + + def get_md_from_file(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] + + if 'MDATA_KEY_TITLE' not in md: + #get rid of file extention from original name, name might have more than 1 '.' in it. + original_name = os.path.basename(filepath) + original_name = original_name.split(".")[0:-1] + original_name = ''.join(original_name) + md['MDATA_KEY_TITLE'] = original_name + + #incase track number is in format u'4/11' + if 'MDATA_KEY_TRACKNUMBER' in md: + if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring): + md['MDATA_KEY_TRACKNUMBER'] = md['MDATA_KEY_TRACKNUMBER'].split("/")[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" + + #do this so object can be urlencoded properly. + for key in md.keys(): + if(isinstance(md[key], basestring)): + md[key] = md[key].encode('utf-8') + + return md + + +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) + + 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() + + self.logger = logging.getLogger('root') + self.api_client = api_client.api_client_factory(config) + self.md_manager = MetadataExtractor() + self.import_processes = {} + self.watched_folders = [] + + def handle_message(self, body, message): + # ACK the message to take it off the queue + message.ack() + + self.logger.info("Received md from RabbitMQ: " + body) + m = json.loads(message.body) + + if m['event_type'] == "md_update": + self.logger.info("AIRTIME NOTIFIER md update event") + self.md_manager.save_md_to_file(m) + elif m['event_type'] == "new_watch": + self.logger.info("AIRTIME NOTIFIER add watched folder event " + m['directory']) + #start a new process to walk through this folder and add the files to Airtime. + p = Process(target=self.walk_newly_watched_directory, args=(m['directory'],)) + p.start() + self.import_processes[m['directory']] = p + #add this new folder to our list of watched folders + self.watched_folders.append(m['directory']) + + 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.md_manager.get_md_from_file(filepath) + md.update(mutagen) + data = md + elif (os.path.exists(filepath) and (mode == MODE_MODIFY)): + mutagen = self.md_manager.get_md_from_file(filepath) + md.update(mutagen) + data = md + elif (mode == MODE_MOVED): + mutagen = self.md_manager.get_md_from_file(filepath) + md.update(mutagen) + data = md + elif (mode == MODE_DELETE): + data = md + + if data is not None: + self.logger.info("Updating Change to Airtime " + filepath) + response = None + while response is None: + response = self.api_client.update_media_metadata(data, mode) + time.sleep(5) + + def walk_newly_watched_directory(self, directory): + + for (path, dirs, files) in os.walk(directory): + for filename in files: + full_filepath = path+"/"+filename + self.update_airtime({'filepath': full_filepath, 'mode': MODE_CREATE}) + + +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.supported_file_formats = ['mp3', 'ogg'] + self.logger = logging.getLogger('root') + self.temp_files = {} + self.moved_files = {} + self.file_events = deque() + self.mask = pyinotify.ALL_EVENTS + self.wm = WatchManager() + self.md_manager = MetadataExtractor() + + 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 set_needed_file_permissions(self, item, is_dir): + + try: + omask = os.umask(0) + + uid = pwd.getpwnam('pypo')[2] + gid = grp.getgrnam('www-data')[2] + + os.chown(item, uid, gid) + + if is_dir is True: + os.chmod(item, 02777) + else: + os.chmod(item, 0666) + + except Exception, e: + self.logger.error("Failed to change file's owner/group/permissions.") + self.logger.error(item) + finally: + os.umask(omask) + def ensure_dir(self, filepath): directory = os.path.dirname(filepath) @@ -196,21 +323,38 @@ class MediaMonitor(ProcessEvent): finally: os.umask(omask) + def move_file(self, source, dest): + + try: + omask = os.umask(0) + os.rename(source, dest) + except Exception, e: + self.logger.error("failed to move file.") + finally: + os.umask(omask) + def create_unique_filename(self, filepath): - if(os.path.exists(filepath)): - file_dir = os.path.dirname(filepath) - filename = os.path.basename(filepath).split(".")[0] - #will be in the format .ext - file_ext = os.path.splitext(filepath)[1] - i = 1; - while(True): - new_filepath = "%s/%s(%s)%s" % (file_dir, filename, i, file_ext) + try: + if(os.path.exists(filepath)): + self.logger.info("Path %s exists", filepath) + file_dir = os.path.dirname(filepath) + filename = os.path.basename(filepath).split(".")[0] + #will be in the format .ext + file_ext = os.path.splitext(filepath)[1] + i = 1; + while(True): + new_filepath = '%s/%s(%s)%s' % (file_dir, filename, i, file_ext) + self.logger.error("Trying %s", new_filepath) - if(os.path.exists(new_filepath)): - i = i+1; - else: - filepath = new_filepath + if(os.path.exists(new_filepath)): + i = i+1; + else: + filepath = new_filepath + break + + except Exception, e: + self.logger.error("Exception %s", e) return filepath @@ -226,23 +370,39 @@ class MediaMonitor(ProcessEvent): #will be in the format .ext file_ext = os.path.splitext(imported_filepath)[1] - md = self.get_mutagen_info(imported_filepath) + file_ext = file_ext.encode('utf-8') + md = self.md_manager.get_md_from_file(imported_filepath) path_md = ['MDATA_KEY_TITLE', 'MDATA_KEY_CREATOR', 'MDATA_KEY_SOURCE', 'MDATA_KEY_TRACKNUMBER', 'MDATA_KEY_BITRATE'] + self.logger.info('Getting md') + for m in path_md: if m not in md: - md[m] = 'unknown' + md[m] = u'unknown'.encode('utf-8') + else: + #get rid of any "/" which will interfere with the filepath. + if isinstance(md[m], basestring): + md[m] = md[m].replace("/", "-") + + self.logger.info(md) + + self.logger.info('Starting filepath creation') filepath = None - if (md['MDATA_KEY_TITLE'] == 'unknown'): - filepath = "%s/%s/%s/%s-%s%s" % (storage_directory, md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], original_name, md['MDATA_KEY_BITRATE'], file_ext) - elif(md['MDATA_KEY_TRACKNUMBER'] == 'unknown'): - filepath = "%s/%s/%s/%s-%s%s" % (storage_directory, md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) + if (md['MDATA_KEY_TITLE'] == u'unknown'.encode('utf-8')): + self.logger.info('unknown title') + filepath = '%s/%s/%s/%s-%s%s' % (storage_directory.encode('utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], original_name, md['MDATA_KEY_BITRATE'], file_ext) + elif(md['MDATA_KEY_TRACKNUMBER'] == u'unknown'.encode('utf-8')): + self.logger.info('unknown track number') + filepath = '%s/%s/%s/%s-%s%s' % (storage_directory.encode('utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) else: - filepath = "%s/%s/%s/%s-%s-%s%s" % (storage_directory, md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TRACKNUMBER'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) + self.logger.info('full metadata') + filepath = '%s/%s/%s/%s-%s-%s%s' % (storage_directory.encode('utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TRACKNUMBER'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext) + self.logger.info(u'Created filepath: %s', filepath) filepath = self.create_unique_filename(filepath) + self.logger.info(u'Unique filepath: %s', filepath) self.ensure_dir(filepath) except Exception, e: @@ -250,63 +410,6 @@ class MediaMonitor(ProcessEvent): return filepath - 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_TRACKNUMBER'] = "%02d" % (int(md['MDATA_KEY_TRACKNUMBER'])) - - 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(".") @@ -334,18 +437,20 @@ class MediaMonitor(ProcessEvent): 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) + if self.is_audio_file(event.pathname): + self.set_needed_file_permissions(event.pathname, event.dir) + md5 = self.md_manager.get_md5(event.pathname) + response = self.api_client.check_media_status(md5) - #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}) + #this file is new, md5 does not exist in Airtime. + if(response['airtime_status'] == 0): + filepath = self.create_file_path(event.pathname) + self.move_file(event.pathname, filepath) + self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath}) - #immediately add a watch on the new directory. else: - self.watch_directory(event.pathname) + self.set_needed_file_permissions(event.pathname, event.dir) + def process_IN_MODIFY(self, event): if not event.dir: @@ -367,6 +472,8 @@ class MediaMonitor(ProcessEvent): def process_IN_MOVED_TO(self, event): self.logger.info("%s: %s", event.maskname, event.pathname) + #if stuff dropped in stor via a UI move must change file permissions. + self.set_needed_file_permissions(event.pathname, event.dir) if not event.dir: if event.cookie in self.temp_files: del self.temp_files[event.cookie] @@ -380,7 +487,7 @@ class MediaMonitor(ProcessEvent): #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.move_file(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}) @@ -389,7 +496,7 @@ class MediaMonitor(ProcessEvent): #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.move_file(event.pathname, md_filepath) self.file_events.append({'mode': MODE_CREATE, 'filepath': md_filepath}) def process_IN_DELETE(self, event): @@ -402,12 +509,22 @@ class MediaMonitor(ProcessEvent): def notifier_loop_callback(self, notifier): + for watched_directory in notifier.import_processes.keys(): + process = notifier.import_processes[watched_directory] + if not process.is_alive(): + self.watch_directory(watched_directory) + del notifier.import_processes[watched_directory] + while len(self.file_events) > 0: + self.logger.info("Processing a file event update to Airtime.") file_info = self.file_events.popleft() - self.update_airtime(file_info) + notifier.update_airtime(file_info) try: notifier.connection.drain_events(timeout=1) + #avoid logging a bunch of timeout messages. + except socket.timeout: + pass except Exception, e: self.logger.info("%s", e) diff --git a/python_apps/media-monitor/airtime-media-monitor-init-d b/python_apps/media-monitor/airtime-media-monitor-init-d index afe45e137..8bf311161 100755 --- a/python_apps/media-monitor/airtime-media-monitor-init-d +++ b/python_apps/media-monitor/airtime-media-monitor-init-d @@ -9,19 +9,21 @@ # Short-Description: Manage airtime-media-monitor daemon ### END INIT INFO -USERID=pypo -GROUPID=pypo -NAME=Airtime +USERID=root +GROUPID=www-data +NAME=Airtime\ Media\ Monitor -DAEMON=/usr/bin/airtime-media-monitor +DAEMON=/usr/lib/airtime/media-monitor/airtime-media-monitor PIDFILE=/var/run/airtime-media-monitor.pid -start () { +start () { + monit monitor airtime-media-monitor >/dev/null 2>&1 start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE --startas $DAEMON } stop () { # Send TERM after 5 seconds, wait at most 30 seconds. + monit unmonitor airtime-media-monitor >/dev/null 2>&1 start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE rm -f $PIDFILE } diff --git a/python_apps/media-monitor/install/media-monitor-install.py b/python_apps/media-monitor/install/media-monitor-install.py index 36f491c57..c6e38b431 100755 --- a/python_apps/media-monitor/install/media-monitor-install.py +++ b/python_apps/media-monitor/install/media-monitor-install.py @@ -34,7 +34,7 @@ def copy_dir(src_dir, 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('/') @@ -60,21 +60,18 @@ try: 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("chmod -R 755 "+config["bin_dir"]+"/airtime-media-monitor) os.system("chown -R pypo:pypo "+config["bin_dir"]) - print "Creating symbolic links" - os.system("rm -f /usr/bin/airtime-media-monitor") - os.system("ln -s "+config["bin_dir"]+"/airtime-media-monitor /usr/bin/") - print "Installing media-monitor daemon" shutil.copy(config["bin_dir"]+"/airtime-media-monitor-init-d", "/etc/init.d/airtime-media-monitor") - p = Popen("update-rc.d airtime-media-monitor defaults", shell=True) + p = Popen("update-rc.d airtime-media-monitor defaults >/dev/null 2>&1", shell=True) sts = os.waitpid(p.pid, 0)[1] - + print "Waiting for processes to start..." p = Popen("/etc/init.d/airtime-media-monitor start", shell=True) sts = os.waitpid(p.pid, 0)[1] diff --git a/python_apps/media-monitor/install/media-monitor-uninstall.py b/python_apps/media-monitor/install/media-monitor-uninstall.py index d2732bffb..db2e07a8a 100755 --- a/python_apps/media-monitor/install/media-monitor-uninstall.py +++ b/python_apps/media-monitor/install/media-monitor-uninstall.py @@ -12,7 +12,7 @@ if os.geteuid() != 0: PATH_INI_FILE = '/etc/airtime/media-monitor.cfg' def remove_path(path): - os.system("rm -rf " + path) + os.system('rm -rf "%s"' % path) def get_current_script_dir(): current_script_dir = os.path.realpath(__file__) @@ -29,7 +29,7 @@ try: os.system("/etc/init.d/airtime-media-monitor stop") os.system("rm -f /etc/init.d/airtime-media-monitor") - os.system("update-rc.d -f airtime-media-monitor remove") + os.system("update-rc.d -f airtime-media-monitor remove >/dev/null 2>&1") print "Removing log directories" remove_path(config["log_dir"]) diff --git a/python_apps/monit/airtime-monit.cfg b/python_apps/monit/airtime-monit.cfg index 10f328e3f..647fffd10 100644 --- a/python_apps/monit/airtime-monit.cfg +++ b/python_apps/monit/airtime-monit.cfg @@ -1,5 +1,10 @@ set daemon 10 # Poll at 10 second intervals set logfile syslog facility log_daemon + + set httpd port 2812 and use address 127.0.0.1 + allow localhost + allow admin:monit + check process airtime-playout with pidfile "/var/run/airtime-playout.pid" start program = "/etc/init.d/airtime-playout start" with timeout 10 seconds diff --git a/python_apps/pypo/airtime-playout-init-d b/python_apps/pypo/airtime-playout-init-d index 5634baa13..b07a97d5d 100755 --- a/python_apps/pypo/airtime-playout-init-d +++ b/python_apps/pypo/airtime-playout-init-d @@ -11,24 +11,31 @@ USERID=pypo GROUPID=pypo -NAME=Airtime +NAME=Airtime\ Playout -DAEMON0=/usr/bin/airtime-playout +DAEMON0=/usr/lib/airtime/pypo/bin/airtime-playout PIDFILE0=/var/run/airtime-playout.pid -DAEMON1=/usr/bin/airtime-liquidsoap +DAEMON1=/usr/lib/airtime/pypo/bin/airtime-liquidsoap PIDFILE1=/var/run/airtime-liquidsoap.pid -start () { +start () { + monit monitor airtime-playout >/dev/null 2>&1 start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE0 --startas $DAEMON0 + + monit monitor airtime-liquidsoap >/dev/null 2>&1 start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE1 --startas $DAEMON1 } stop () { # Send TERM after 5 seconds, wait at most 30 seconds. + + monit unmonitor airtime-playout >/dev/null 2>&1 start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE0 - start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE1 rm -f $PIDFILE0 + + monit unmonitor airtime-liquidsoap >/dev/null 2>&1 + start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE1 rm -f $PIDFILE1 } diff --git a/python_apps/pypo/install/pypo-install.py b/python_apps/pypo/install/pypo-install.py index 32f3f873a..5b8df115c 100755 --- a/python_apps/pypo/install/pypo-install.py +++ b/python_apps/pypo/install/pypo-install.py @@ -81,10 +81,10 @@ try: if architecture == '64bit' and natty: print "Installing 64-bit liquidsoap binary (Natty)" - shutil.copy("%s/../liquidsoap_bin/liquidsoap-amd64-natty"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir) + shutil.copy("%s/../liquidsoap_bin/liquidsoap-natty-amd64"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir) elif architecture == '32bit' and natty: print "Installing 32-bit liquidsoap binary (Natty)" - shutil.copy("%s/../liquidsoap_bin/liquidsoap-i386-natty"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir) + shutil.copy("%s/../liquidsoap_bin/liquidsoap-natty-i386"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir) elif architecture == '64bit' and not natty: print "Installing 64-bit liquidsoap binary" shutil.copy("%s/../liquidsoap_bin/liquidsoap-amd64"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir) @@ -103,16 +103,10 @@ try: os.system("chown -R pypo:pypo "+config["bin_dir"]) os.system("chown -R pypo:pypo "+config["cache_base_dir"]) - print "Creating symbolic links" - os.system("rm -f /usr/bin/airtime-playout") - os.system("ln -s "+config["bin_dir"]+"/bin/airtime-playout /usr/bin/") - os.system("rm -f /usr/bin/airtime-liquidsoap") - os.system("ln -s "+config["bin_dir"]+"/bin/airtime-liquidsoap /usr/bin/") - print "Installing pypo daemon" shutil.copy(config["bin_dir"]+"/bin/airtime-playout-init-d", "/etc/init.d/airtime-playout") - p = Popen("update-rc.d airtime-playout defaults", shell=True) + p = Popen("update-rc.d airtime-playout defaults >/dev/null 2>&1", shell=True) sts = os.waitpid(p.pid, 0)[1] print "Waiting for processes to start..." diff --git a/python_apps/pypo/install/pypo-uninstall.py b/python_apps/pypo/install/pypo-uninstall.py index bfa77eb0e..d2596099b 100755 --- a/python_apps/pypo/install/pypo-uninstall.py +++ b/python_apps/pypo/install/pypo-uninstall.py @@ -12,7 +12,7 @@ if os.geteuid() != 0: PATH_INI_FILE = '/etc/airtime/pypo.cfg' def remove_path(path): - os.system("rm -rf " + path) + os.system('rm -rf "%s"' % path) def get_current_script_dir(): current_script_dir = os.path.realpath(__file__) @@ -29,7 +29,7 @@ try: os.system("/etc/init.d/airtime-playout stop") os.system("rm -f /etc/init.d/airtime-playout") - os.system("update-rc.d -f airtime-playout remove") + os.system("update-rc.d -f airtime-playout remove >/dev/null 2>&1") print "Removing cache directories" remove_path(config["cache_base_dir"]) diff --git a/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64 b/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64 index ddb0b7aa1..453244f14 100755 Binary files a/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64 and b/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64 differ diff --git a/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64-natty b/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64-natty deleted file mode 100755 index 7aca06e45..000000000 Binary files a/python_apps/pypo/liquidsoap_bin/liquidsoap-amd64-natty and /dev/null differ diff --git a/python_apps/pypo/liquidsoap_bin/liquidsoap-i386 b/python_apps/pypo/liquidsoap_bin/liquidsoap-i386 index 9a28b85e4..5768d6384 100755 Binary files a/python_apps/pypo/liquidsoap_bin/liquidsoap-i386 and b/python_apps/pypo/liquidsoap_bin/liquidsoap-i386 differ diff --git a/python_apps/pypo/liquidsoap_bin/liquidsoap-i386-natty b/python_apps/pypo/liquidsoap_bin/liquidsoap-i386-natty deleted file mode 100755 index ff22395fc..000000000 Binary files a/python_apps/pypo/liquidsoap_bin/liquidsoap-i386-natty and /dev/null differ diff --git a/python_apps/pypo/liquidsoap_bin/liquidsoap-natty-amd64 b/python_apps/pypo/liquidsoap_bin/liquidsoap-natty-amd64 new file mode 100755 index 000000000..17dbcfa67 Binary files /dev/null and b/python_apps/pypo/liquidsoap_bin/liquidsoap-natty-amd64 differ diff --git a/python_apps/pypo/liquidsoap_bin/liquidsoap-natty-i386 b/python_apps/pypo/liquidsoap_bin/liquidsoap-natty-i386 new file mode 100755 index 000000000..d2c339597 Binary files /dev/null and b/python_apps/pypo/liquidsoap_bin/liquidsoap-natty-i386 differ diff --git a/python_apps/pypo/liquidsoap_scripts/library/Makefile b/python_apps/pypo/liquidsoap_scripts/library/Makefile index cc4c77cda..d10777180 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/Makefile +++ b/python_apps/pypo/liquidsoap_scripts/library/Makefile @@ -1,4 +1,5 @@ +SUBDIRS = tests DISTFILES = $(wildcard *.in) Makefile ask-liquidsoap.rb ask-liquidsoap.pl \ $(wildcard *.liq) extract-replaygain diff --git a/python_apps/pypo/liquidsoap_scripts/library/externals.liq b/python_apps/pypo/liquidsoap_scripts/library/externals.liq index ede1d2e3d..1a50f4b95 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/externals.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/externals.liq @@ -9,37 +9,43 @@ my_get_mime = get_mime get_mime = my_get_mime %ifdef add_decoder -if test_process("which flac") then - log(level=3,"Found flac binary: enabling flac external decoder.") - flac_p = "flac -d -c - 2>/dev/null" - def test_flac(file) = - if test_process("which metaflac") then - channels = list.hd(get_process_lines("metaflac \ - --show-channels #{quote(file)} \ - 2>/dev/null")) - # If the value is not an int, this returns 0 and we are ok :) - int_of_string(channels) - else - # Try to detect using mime test.. - mime = get_mime(file) - if string.match(pattern="flac",file) then - # We do not know the number of audio channels - # so setting to -1 - (-1) +# Enable external FLAC decoders. Requires flac binary +# in the path for audio decoding and metaflac binary for +# metadata. Does not work on Win32. Default: disabled. +# Please note that built-in support for FLAC is available +# in liquidsoap if compiled and should be preferred over +# the external decoder. +# @category Liquidsoap +def enable_external_flac_decoder() = + if test_process("which flac") then + log(level=3,"Found flac binary: enabling flac external decoder.") + flac_p = "flac -d -c - 2>/dev/null" + def test_flac(file) = + if test_process("which metaflac") then + channels = list.hd(get_process_lines("metaflac \ + --show-channels #{quote(file)} \ + 2>/dev/null")) + # If the value is not an int, this returns 0 and we are ok :) + int_of_string(channels) else - # All tests failed: no audio decodable using flac.. - 0 + # Try to detect using mime test.. + mime = get_mime(file) + if string.match(pattern="flac",file) then + # We do not know the number of audio channels + # so setting to -1 + (-1) + else + # All tests failed: no audio decodable using flac.. + 0 + end end end + add_decoder(name="FLAC",description="Decode files using the flac \ + decoder binary.", test=test_flac,flac_p) + else + log(level=3,"Did not find flac binary: flac decoder disabled.") end - add_decoder(name="FLAC",description="Decode files using the flac \ - decoder binary.", test=test_flac,flac_p) -else - log(level=3,"Did not find flac binary: flac decoder disabled.") -end -%endif -if os.type != "Win32" then if test_process("which metaflac") then log(level=3,"Found metaflac binary: enabling flac external metadata \ resolver.") @@ -55,49 +61,59 @@ if os.type != "Win32" then if list.length(l) >= 1 then list.append([(list.hd(l),"")],l') else - l' - end + l' end end - list.fold(f,[],ret) + end + list.fold(f,[],ret) end add_metadata_resolver("FLAC",flac_meta) else - log(level=3,"Did not find metaflac binary: flac metadata resolver disabled.") + log(level=3,"Did not find metaflac binary: flac metadata resolver disabled.") end end +%endif -# A list of know extensions and content-type for AAC. -# Values from http://en.wikipedia.org/wiki/Advanced_Audio_Coding -# TODO: can we register a setting for that ?? -aac_mimes = - ["audio/aac", "audio/aacp", "audio/3gpp", "audio/3gpp2", "audio/mp4", - "audio/MP4A-LATM", "audio/mpeg4-generic", "audio/x-hx-aac-adts"] -aac_filexts = ["m4a", "m4b", "m4p", "m4v", - "m4r", "3gp", "mp4", "aac"] +%ifdef add_oblivious_decoder +# Enable or disable external FAAD (AAC/AAC+/M4A) decoders. +# Requires faad binary in the path for audio decoding and +# metaflac binary for metadata. Does not work on Win32. +# Please note that built-in support for faad is available +# in liquidsoap if compiled and should be preferred over +# the external decoder. +# @category Liquidsoap +def enable_external_faad_decoder() = -# Faad is not very selective so -# We are checking only file that -# end with a known extension or mime type -def faad_test(file) = - # Get the file's mime - mime = get_mime(file) - # Test mime - if list.mem(mime,aac_mimes) then - true - else - # Otherwise test file extension - ret = string.extract(pattern='\.(.+)$',file) + # A list of know extensions and content-type for AAC. + # Values from http://en.wikipedia.org/wiki/Advanced_Audio_Coding + # TODO: can we register a setting for that ?? + aac_mimes = + ["audio/aac", "audio/aacp", "audio/3gpp", "audio/3gpp2", "audio/mp4", + "audio/MP4A-LATM", "audio/mpeg4-generic", "audio/x-hx-aac-adts"] + aac_filexts = ["m4a", "m4b", "m4p", "m4v", + "m4r", "3gp", "mp4", "aac"] + + # Faad is not very selective so + # We are checking only file that + # end with a known extension or mime type + def faad_test(file) = + # Get the file's mime + mime = get_mime(file) + # Test mime + if list.mem(mime,aac_mimes) then + true + else + # Otherwise test file extension + ret = string.extract(pattern='\.(.+)$',file) if list.length(ret) != 0 then - ext = ret["1"] - list.mem(ext,aac_filexts) - else - false - end + ext = ret["1"] + list.mem(ext,aac_filexts) + else + false + end + end end -end -if os.type != "Win32" then if test_process("which faad") then log(level=3,"Found faad binary: enabling external faad decoder and \ metadata resolver.") @@ -120,15 +136,13 @@ if os.type != "Win32" then 0 end end -%ifdef add_oblivious_decoder add_oblivious_decoder(name="FAAD",description="Decode files using \ the faad binary.", test=test_faad, faad_p) -%endif def faad_meta(file) = if faad_test(file) then ret = get_process_lines("faad -i \ #{quote(file)} 2>&1") - # Yea, this is tuff programming (again) ! + # Yea, this is ugly programming (again) ! def get_meta(l,s)= ret = string.extract(pattern="^(\w+):\s(.+)$",s) if list.length(ret) > 0 then @@ -147,6 +161,7 @@ if os.type != "Win32" then log(level=3,"Did not find faad binary: faad decoder disabled.") end end +%endif # Standard function for displaying metadata. # Shows artist and title, using "Unknown" when a field is empty. @@ -189,3 +204,22 @@ def notify_metadata(~urgency="low",~icon="stock_smiley-22",~time=3000, ^ ' -t #{time} #{quote(title)} ' on_metadata(fun (m) -> system(send^quote(display(m))),s) end + +%ifdef input.external +# Stream data from mplayer +# @category Source / Input +# @param s data URI. +# @param ~restart restart on exit. +# @param ~restart_on_error restart on exit with error. +# @param ~buffer Duration of the pre-buffered data. +# @param ~max Maximum duration of the buffered data. +def input.mplayer(~id="input.mplayer", + ~restart=true,~restart_on_error=false, + ~buffer=0.2,~max=10.,s) = + input.external(id=id,restart=restart, + restart_on_error=restart_on_error, + buffer=buffer,max=max, + "mplayer -really-quiet -ao pcm:file=/dev/stdout \ + -vc null -vo null #{quote(s)} 2>/dev/null") +end +%endif diff --git a/python_apps/pypo/liquidsoap_scripts/library/liquidsoap.initd b/python_apps/pypo/liquidsoap_scripts/library/liquidsoap.initd old mode 100755 new mode 100644 diff --git a/python_apps/pypo/liquidsoap_scripts/library/test.liq b/python_apps/pypo/liquidsoap_scripts/library/test.liq deleted file mode 100644 index ac78f2037..000000000 --- a/python_apps/pypo/liquidsoap_scripts/library/test.liq +++ /dev/null @@ -1,33 +0,0 @@ -set("log.file",false) - -echo = fun (x) -> system("echo "^quote(x)) - -def test(lbl,f) - if f() then echo(lbl) else system("echo fail "^lbl) end -end - -test("1",{ 1==1 }) -test("2",{ 1+1==2 }) -test("3",{ (-1)+2==1 }) -test("4",{ (-1)+2 <= 3*2 }) -test("5",{ true }) -test("6",{ true and true }) -test("7",{ 1==1 and 1==1 }) -test("8",{ (1==1) and (1==1) }) -test("9",{ true and (-1)+2 <= 3*2 }) - -l = [ ("bla",""), ("bli","x"), ("blo","xx"), ("blu","xxx"), ("dix","10") ] -echo(l["dix"]) -test("11",{ 2 == list.length(string.split(separator="",l["blo"])) }) - -%ifdef foobarbaz - if = if is not a well-formed expression, and we do not care... -%endif - -echo("1#{1+1}") -echo(string_of(int_of_float(float_of_string(default=13.,"blah")))) - -f=fun(x)->x -# Checking that the following is not recursive: -f=fun(x)->f(x) -print(f(14)) diff --git a/python_apps/pypo/liquidsoap_scripts/library/typing.liq b/python_apps/pypo/liquidsoap_scripts/library/typing.liq deleted file mode 100644 index 2a4c7eaa7..000000000 --- a/python_apps/pypo/liquidsoap_scripts/library/typing.liq +++ /dev/null @@ -1,112 +0,0 @@ -# Check these examples with: liquidsoap --no-libs -i -c typing.liq - -# TODO Throughout this file, parsing locations displayed in error messages -# are often much too inaccurate. - -set("log.file",false) - -# Check that some polymorphism is allowed. -# id :: (string,'a)->'a -def id(a,b) - log(a) - b -end -ignore("bla"==id("bla","bla")) -ignore(0==id("zero",0)) - -# Reporting locations for the next error is non-trivial, because it is about -# an instantiation of the type of id. The deep error doesn't have relevant -# information about why the int should be a string, the outer one has. -# id(0,0) - -# Polymorphism is limited to outer generalizations, this is not system F. -# apply :: ((string)->'a)->'a -def apply(f) - f("bla") - # f is not polymorphic, the following is forbidden: - # f(0) - # f(f) -end - -# The level checks forbid abusive generalization. -# id' :: ('a)->'a -def id'(x) - # If one isn't careful about levels/scoping, f2 gets the type ('a)->'b - # and so does twisted_id. - def f2(y) - x - end - f2(x) -end - -# More errors... -# 0=="0" -# [3,""] - -# Subtyping, functions and lists. -f1 = fun () -> 3 -f2 = fun (a=1) -> a - -# This is OK, l1 is a list of elements of type f1. -l1 = [f1,f2] -list.iter(fun (f) -> log(string_of(f())), l1) -# Forbidden. Indeed, f1 doesn't accept any argument -- although f2 does. -# Here the error message may even be too detailed since it goes back to the -# definition of l1 and requires that f1 has type (int)->int. -# list.iter(fun (f) -> log(string_of(f(42))), l1) - -# Actually, this is forbidden too, but the reason is more complex... -# The infered type for the function is ((int)->int)->unit, -# and (int)->int is not a subtype of (?int)->int. -# There's no most general answer here since (?int)->int is not a -# subtype of (int)->int either. -# list.iter(fun (f) -> log(string_of(f(42))), [f2]) - -# Unlike l1, this is not OK, since we don't leave open subtyping constraints -# while infering types. -# I hope we can make the inference smarter in the future, without obfuscating -# the error messages too much. -# The type error here shows the use of all the displayed positions: -# f1 has type t1, f2 has type t2, t1 should be <: t2 -# l2 = [ f2, f1 ] - -# An error where contravariance flips the roles of both sides.. -# [fun (x) -> x+1, fun (y) -> y^"."] - -# An error without much locations.. -# TODO An explaination about the missing label would help a lot here. -# def f(f) -# f(output.icecast.vorbis) -# f(output.icecast.mp3) -# end - -# This causes an occur-check error. -# TODO The printing of the types breaks the sharing of one EVAR -# across two types. Here the sharing is actually the origin of the occur-check -# error. And it's not easy to understand.. -# omega = fun (x) -> x(x) - -# Now let's test ad-hoc polymorphism. - -echo = fun(x) -> system("echo #{quote(string_of(x))}") - -echo("bla") -echo((1,3.12)) -echo(1 + 1) -echo(1. + 2.14) - -# string is not a Num -# echo("bl"+"a") - -echo(1 <= 2) -echo((1,2) == (1,3)) - -# float <> int -# echo(1 == 2.) - -# source is not an Ord -# echo(blank()==blank()) - -def sum_eq(a,b) - a+b == a -end diff --git a/python_apps/pypo/liquidsoap_scripts/library/utils.liq b/python_apps/pypo/liquidsoap_scripts/library/utils.liq index da4224dc0..fe1963100 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/utils.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/utils.liq @@ -300,6 +300,25 @@ def server.insert_metadata(~id="",s) = s end +# Register a command that outputs the RMS of the returned source. +# @category Source / Visualization +# @param ~id Force the value of the source ID. +def server.rms(~id="",s) = + x = rms(id=id,s) + rms = fst(x) + s = snd(x) + id = source.id(s) + def rms(_) = + rms = rms() + "#{rms}" + end + server.register(namespace="#{id}", + description="Return the current RMS of the source.", + usage="rms", + "rms",rms) + s +end + # Get the base name of a path. # Implemented using the corresponding shell command. # @category System @@ -479,59 +498,95 @@ def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3., end # Custom playlist source written using the script language. -# Will read directory or playlist, play all files and stop +# Will read directory or playlist, play all files and stop. +# Returns a pair @(reload,source)@ where @reload@ is a function +# of type @(?uri:string)->unit@ used to reload the source and @source@ +# is the actual source. The reload function can optionally be called +# with a new playlist URI. Otherwise, it reloads the previous URI. # @category Source / Input +# @param ~id Force the value of the source ID. # @param ~random Randomize playlist content # @param ~on_done Function to execute when the playlist is finished # @param uri Playlist URI -def playlist.once(~random=false,~on_done={()},uri) - x = ref 0 - def playlist.custom(files) - length = list.length(files) - if length == 0 then - log("Empty playlist..") - fail () - else - files = - if random then - list.sort(fun (x,y) -> int_of_float(random.float()), files) - else - files - end - def next () = - state = !x - file = - if state < length then - x := state + 1 - list.nth(files,state) - else - # Playlist finished +def playlist.reloadable(~id="",~random=false,~on_done={()},uri) + # A reference to the playlist + playlist = ref [] + # A reference to the uri + playlist_uri = ref uri + # A reference to know if the source + # has been stopped + has_stopped = ref false + # The next function + def next () = + file = + if list.length(!playlist) > 0 then + ret = list.hd(!playlist) + playlist := list.tl(!playlist) + ret + else + # Playlist finished + if not !has_stopped then on_done () - "" end - request.create(file) + has_stopped := true + "" end - request.dynamic(next) + request.create(file) + end + # Instanciate the source + source = request.dynamic(id=id,next) + # Get its id. + id = source.id(source) + # The load function + def load_playlist () = + files = + if test_process("test -d #{quote(!playlist_uri)}") then + log(label=id,"playlist is a directory.") + get_process_lines("find #{quote(!playlist_uri)} -type f | sort") + else + playlist = request.create.raw(!playlist_uri) + result = + if request.resolve(playlist) then + playlist = request.filename(playlist) + files = playlist.parse(playlist) + list.map(snd,files) + else + log(label=id,"Couldn't read playlist: request resolution failed.") + [] + end + request.destroy(playlist) + result + end + if random then + playlist := list.sort(fun (x,y) -> int_of_float(random.float()), files) + else + playlist := files end end - if test_process("test -d #{quote(uri)}") then - files = get_process_lines("find #{quote(uri)} -type f | sort") - playlist.custom(files) - else - playlist = request.create.raw(uri) - result = - if request.resolve(playlist) then - playlist = request.filename(playlist) - files = playlist.parse(playlist) - files = list.map(snd,files) - playlist.custom(files) - else - log("Couldn't read playlist: request resolution failed.") - fail () - end - request.destroy(playlist) - result + # The reload function + def reload(~uri="") = + if uri != "" then + playlist_uri := uri + end + log(label=id,"Reloading playlist with URI #{!playlist_uri}") + has_stopped := false + load_playlist() end + # Load the playlist + load_playlist() + # Return + (reload,source) +end + +# Custom playlist source written using the script language. +# Will read directory or playlist, play all files and stop +# @category Source / Input +# @param ~id Force the value of the source ID. +# @param ~random Randomize playlist content +# @param ~on_done Function to execute when the playlist is finished +# @param uri Playlist URI +def playlist.once(~id="",~random=false,~on_done={()},uri) + snd(playlist.reloadable(id=id,random=random,on_done=on_done,uri)) end # Mixes two streams, with faded transitions between the state when only the @@ -588,7 +643,8 @@ def exec_at(~freq=1.,~pred,f) add_timeout(freq,check) end -# Register the replaygain protocol +# Register the replaygain protocol. +# @category Liquidsoap def replaygain_protocol(arg,delay) # The extraction program extract_replaygain = "#{configure.libdir}/extract-replaygain" diff --git a/python_apps/pypo/liquidsoap_scripts/liquidsoap.cfg b/python_apps/pypo/liquidsoap_scripts/liquidsoap.cfg index 98436d03a..b0b13b0bc 100644 --- a/python_apps/pypo/liquidsoap_scripts/liquidsoap.cfg +++ b/python_apps/pypo/liquidsoap_scripts/liquidsoap.cfg @@ -1,46 +1,51 @@ ########################################### -# liquidsoap config file # +# Liquidsoap config file # ########################################### +########################################### +# Output settings # +########################################### +output_sound_device = false +output_icecast_vorbis = true +output_icecast_mp3 = false +output_shoutcast = false ########################################### -# general settings # +# Logging settings # ########################################### - log_file = "/var/log/airtime/pypo-liquidsoap/