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()) : ?>
+
+ element->getElement('watchedFolder')->getMessages() as $error): ?>
+
+
+
+
+
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/