Merge branch 'devel' of dev.sourcefabric.org:airtime into devel

Conflicts:
	airtime_mvc/application/controllers/PreferenceController.php
	airtime_mvc/application/models/Preference.php
This commit is contained in:
james 2011-06-20 18:09:37 -04:00
commit 05dc48fa71
55 changed files with 907 additions and 664 deletions

View File

@ -2,6 +2,10 @@
CREDITS CREDITS
======= =======
Version 1.9.0
-------------
Same as previous version.
Version 1.8.2 Version 1.8.2
------------- -------------
Welcome to James Moon! Welcome to James Moon!

View File

@ -54,7 +54,7 @@ class ApiController extends Zend_Controller_Action
* Allows remote client to download requested media file. * Allows remote client to download requested media file.
* *
* @return void * @return void
* The given value increased by the increment amount. *
*/ */
public function getMediaAction() public function getMediaAction()
{ {
@ -65,7 +65,7 @@ class ApiController extends Zend_Controller_Action
$this->_helper->viewRenderer->setNoRender(true); $this->_helper->viewRenderer->setNoRender(true);
$api_key = $this->_getParam('api_key'); $api_key = $this->_getParam('api_key');
$downlaod = $this->_getParam('download'); $download = ("true" == $this->_getParam('download'));
if(!in_array($api_key, $CC_CONFIG["apiKey"])) if(!in_array($api_key, $CC_CONFIG["apiKey"]))
{ {
@ -87,7 +87,6 @@ class ApiController extends Zend_Controller_Action
exit; exit;
} }
// possibly use fileinfo module here in the future. // possibly use fileinfo module here in the future.
// http://www.php.net/manual/en/book.fileinfo.php // http://www.php.net/manual/en/book.fileinfo.php
$ext = pathinfo($filename, PATHINFO_EXTENSION); $ext = pathinfo($filename, PATHINFO_EXTENSION);
@ -96,7 +95,12 @@ class ApiController extends Zend_Controller_Action
else if ($ext == "mp3") else if ($ext == "mp3")
header("Content-Type: audio/mpeg"); header("Content-Type: audio/mpeg");
if ($download){ 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)); header("Content-Length: " . filesize($filepath));
@ -408,7 +412,8 @@ class ApiController extends Zend_Controller_Action
public function reloadMetadataAction() { public function reloadMetadataAction() {
global $CC_CONFIG; 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"])) if (!in_array($api_key, $CC_CONFIG["apiKey"]))
{ {
header('HTTP/1.0 401 Unauthorized'); header('HTTP/1.0 401 Unauthorized');
@ -416,8 +421,16 @@ class ApiController extends Zend_Controller_Action
exit; exit;
} }
$md = $this->_getParam('md'); $mode = $request->getParam('mode');
$mode = $this->_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") { if ($mode == "create") {
$md5 = $md['MDATA_KEY_MD5']; $md5 = $md['MDATA_KEY_MD5'];

View File

@ -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/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/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.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/library.js','text/javascript');
$this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/advancedsearch.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(); $data = $file->getMetadata();
RabbitMq::SendFileMetaData($data); RabbitMq::SendMessageToMediaMonitor("md_update", $data);
$this->_helper->redirector('index'); $this->_helper->redirector('index');
} }

View File

@ -113,8 +113,8 @@ class PlaylistController extends Zend_Controller_Action
$this->changePlaylist($pl_id); $this->changePlaylist($pl_id);
$pl = $this->getPlaylist(); $pl = $this->getPlaylist();
$title = $pl->getPLMetaData(UI_MDATA_KEY_TITLE); $title = $pl->getPLMetaData("dc:title");
$desc = $pl->getPLMetaData(UI_MDATA_KEY_DESCRIPTION); $desc = $pl->getPLMetaData("dc:description");
$data = array( 'title' => $title, 'description' => $desc); $data = array( 'title' => $title, 'description' => $desc);
$form->populate($data); $form->populate($data);
@ -130,7 +130,7 @@ class PlaylistController extends Zend_Controller_Action
$pl->setName($title); $pl->setName($title);
if(isset($description)) { if(isset($description)) {
$pl->setPLMetaData(UI_MDATA_KEY_DESCRIPTION, $description); $pl->setPLMetaData("dc:description", $description);
} }
$this->view->pl = $pl; $this->view->pl = $pl;

View File

@ -19,23 +19,24 @@ class PreferenceController extends Zend_Controller_Action
$this->view->headScript()->appendFile($baseUrl.'/js/airtime/preferences/preferences.js','text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/airtime/preferences/preferences.js','text/javascript');
$this->view->statusMsg = ""; $this->view->statusMsg = "";
$form = new Application_Form_Preferences(); $form = new Application_Form_Preferences();
if ($request->isPost()) { if ($request->isPost()) {
if ($form->isValid($request->getPost())) { if ($form->isValid($request->getPost())) {
$values = $form->getValues(); $values = $form->getValues();
Application_Model_Preference::SetHeadTitle($values["preferences_general"]["stationName"], $this->view); Application_Model_Preference::SetHeadTitle($values["preferences_general"]["stationName"], $this->view);
Application_Model_Preference::SetDefaultFade($values["preferences_general"]["stationDefaultFade"]); Application_Model_Preference::SetDefaultFade($values["preferences_general"]["stationDefaultFade"]);
Application_Model_Preference::SetStreamLabelFormat($values["preferences_general"]["streamFormat"]); Application_Model_Preference::SetStreamLabelFormat($values["preferences_general"]["streamFormat"]);
Application_Model_Preference::SetAllow3rdPartyApi($values["preferences_general"]["thirdPartyApi"]); 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::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::SetSoundCloudTags($values["preferences_soundcloud"]["SoundCloudTags"]);
Application_Model_Preference::SetSoundCloudGenre($values["preferences_soundcloud"]["SoundCloudGenre"]); Application_Model_Preference::SetSoundCloudGenre($values["preferences_soundcloud"]["SoundCloudGenre"]);
Application_Model_Preference::SetSoundCloudTrackType($values["preferences_soundcloud"]["SoundCloudTrackType"]); 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::SetStationDescription($values["preferences_support"]["Description"]);
Application_Model_Preference::SetStationLogo($imagePath); Application_Model_Preference::SetStationLogo($imagePath);
$data = array();
$data["directory"] = $values["preferences_general"]["watchedFolder"];
RabbitMq::SendMessageToMediaMonitor("new_watch", $data);
$this->view->statusMsg = "<div class='success'>Preferences updated.</div>"; $this->view->statusMsg = "<div class='success'>Preferences updated.</div>";
} }
} }
$this->view->supportFeedback = Application_Model_Preference::GetSupportFeedback(); $this->view->supportFeedback = Application_Model_Preference::GetSupportFeedback();
$logo = Application_Model_Preference::GetStationLogo(); $logo = Application_Model_Preference::GetStationLogo();
if($logo){ if($logo){

View File

@ -33,15 +33,15 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
'label' => 'Default Fade:', 'label' => 'Default Fade:',
'required' => false, 'required' => false,
'filters' => array('StringTrim'), 'filters' => array('StringTrim'),
'validators' => array(array('regex', false, 'validators' => array(array('regex', false,
array('/^[0-2][0-3]:[0-5][0-9]:[0-5][0-9](\.\d{1,6})?$/', array('/^[0-2][0-3]:[0-5][0-9]:[0-5][0-9](\.\d{1,6})?$/',
'messages' => 'enter a time 00:00:00{.000000}'))), 'messages' => 'enter a time 00:00:00{.000000}'))),
'value' => $defaultFade, 'value' => $defaultFade,
'decorators' => array( 'decorators' => array(
'ViewHelper' 'ViewHelper'
) )
)); ));
$stream_format = new Zend_Form_Element_Radio('streamFormat'); $stream_format = new Zend_Form_Element_Radio('streamFormat');
$stream_format->setLabel('Stream Label:'); $stream_format->setLabel('Stream Label:');
$stream_format->setMultiOptions(array("Artist - Title", $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->setValue(Application_Model_Preference::GetAllow3rdPartyApi());
$third_party_api->setDecorators(array('ViewHelper')); $third_party_api->setDecorators(array('ViewHelper'));
$this->addElement($third_party_api); $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'
)
));
} }

View File

@ -127,5 +127,35 @@ class DateHelper
$explode = explode(" ", $p_timestamp); $explode = explode(" ", $p_timestamp);
return $explode[1]; 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;
}
} }

View File

@ -393,7 +393,7 @@ class Playlist {
->orderByDbPosition() ->orderByDbPosition()
->filterByDbPlaylistId($this->id) ->filterByDbPlaylistId($this->id)
->find(); ->find();
$i = 0; $i = 0;
$offset = 0; $offset = 0;
foreach ($rows as $row) { foreach ($rows as $row) {
@ -502,7 +502,7 @@ class Playlist {
} }
$metadata = $media->getMetadata(); $metadata = $media->getMetadata();
$length = $metadata["dcterms:extent"]; $length = $metadata['MDATA_KEY_DURATION'];
if (!is_null($p_clipLength)) { if (!is_null($p_clipLength)) {
$length = $p_clipLength; $length = $p_clipLength;

View File

@ -36,7 +36,7 @@ class Application_Model_Preference
else if(is_null($id)) { else if(is_null($id)) {
$sql = "INSERT INTO cc_pref (keystr, valstr)" $sql = "INSERT INTO cc_pref (keystr, valstr)"
." VALUES ('$key', '$value')"; ." VALUES ('$key', '$value')";
} }
else { else {
$sql = "INSERT INTO cc_pref (subjid, keystr, valstr)" $sql = "INSERT INTO cc_pref (subjid, keystr, valstr)"
." VALUES ($id, '$key', '$value')"; ." VALUES ($id, '$key', '$value')";
@ -188,6 +188,7 @@ class Application_Model_Preference
return $val; return $val;
} }
} }
<<<<<<< HEAD
public static function SetPhone($phone){ public static function SetPhone($phone){
Application_Model_Preference::SetValue("phone", $phone); Application_Model_Preference::SetValue("phone", $phone);
@ -350,5 +351,16 @@ class Application_Model_Preference
return Application_Model_Preference::GetValue("remindme"); 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
} }

View File

@ -40,10 +40,12 @@ class RabbitMq
} }
} }
public static function SendFileMetaData($md) public static function SendMessageToMediaMonitor($event_type, $md)
{ {
global $CC_CONFIG; global $CC_CONFIG;
$md["event_type"] = $event_type;
$conn = new AMQPConnection($CC_CONFIG["rabbitmq"]["host"], $conn = new AMQPConnection($CC_CONFIG["rabbitmq"]["host"],
$CC_CONFIG["rabbitmq"]["port"], $CC_CONFIG["rabbitmq"]["port"],
$CC_CONFIG["rabbitmq"]["user"], $CC_CONFIG["rabbitmq"]["user"],

View File

@ -184,7 +184,9 @@ class ScheduleGroup {
." st.cue_out," ." st.cue_out,"
." st.clip_length," ." st.clip_length,"
." st.fade_in," ." st.fade_in,"
." st.fade_out" ." st.fade_out,"
." st.starts,"
." st.ends"
." FROM $CC_CONFIG[scheduleTable] as st" ." FROM $CC_CONFIG[scheduleTable] as st"
." LEFT JOIN $CC_CONFIG[showInstances] as si" ." LEFT JOIN $CC_CONFIG[showInstances] as si"
." ON st.instance_id = si.id" ." ON st.instance_id = si.id"
@ -676,7 +678,7 @@ class Schedule {
$timestamp = strtotime($start); $timestamp = strtotime($start);
$playlists[$pkey]['source'] = "PLAYLIST"; $playlists[$pkey]['source'] = "PLAYLIST";
$playlists[$pkey]['x_ident'] = $dx['group_id']; $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]['timestamp'] = $timestamp;
$playlists[$pkey]['duration'] = $dx['clip_length']; $playlists[$pkey]['duration'] = $dx['clip_length'];
$playlists[$pkey]['played'] = '0'; $playlists[$pkey]['played'] = '0';
@ -696,27 +698,24 @@ class Schedule {
$scheduleGroup = new ScheduleGroup($playlist["schedule_id"]); $scheduleGroup = new ScheduleGroup($playlist["schedule_id"]);
$items = $scheduleGroup->getItems(); $items = $scheduleGroup->getItems();
$medias = array(); $medias = array();
$playlist['subtype'] = '1';
foreach ($items as $item) foreach ($items as $item)
{ {
$storedFile = StoredFile::Recall($item["file_id"]); $storedFile = StoredFile::Recall($item["file_id"]);
$uri = $storedFile->getFileUrl(); $uri = $storedFile->getFileUrl();
// For pypo, a cueout of zero means no cueout $starts = Schedule::AirtimeTimeToPypoTime($item["starts"]);
$cueOut = "0"; $medias[$starts] = array(
if (Schedule::TimeDiff($item["cue_out"], $item["clip_length"]) > 0.001) {
$cueOut = Schedule::WallTimeToMillisecs($item["cue_out"]);
}
$medias[] = array(
'row_id' => $item["id"], 'row_id' => $item["id"],
'id' => $storedFile->getGunid(), 'id' => $storedFile->getGunid(),
'uri' => $uri, 'uri' => $uri,
'fade_in' => Schedule::WallTimeToMillisecs($item["fade_in"]), 'fade_in' => Schedule::WallTimeToMillisecs($item["fade_in"]),
'fade_out' => Schedule::WallTimeToMillisecs($item["fade_out"]), 'fade_out' => Schedule::WallTimeToMillisecs($item["fade_out"]),
'fade_cross' => 0, 'fade_cross' => 0,
'cue_in' => Schedule::WallTimeToMillisecs($item["cue_in"]), 'cue_in' => DateHelper::CalculateLengthInSeconds($item["cue_in"]),
'cue_out' => $cueOut, 'cue_out' => DateHelper::CalculateLengthInSeconds($item["cue_out"]),
'export_source' => 'scheduler' 'export_source' => 'scheduler',
'start' => $starts,
'end' => Schedule::AirtimeTimeToPypoTime($item["ends"])
); );
} }
$playlist['medias'] = $medias; $playlist['medias'] = $medias;

View File

@ -63,6 +63,10 @@ class StoredFile {
return $this->_file->getDbFtype(); return $this->_file->getDbFtype();
} }
public function getPropelOrm(){
return $this->_file;
}
public function setFormat($p_format) public function setFormat($p_format)
{ {
$this->_file->setDbFtype($p_format); $this->_file->setDbFtype($p_format);

View File

@ -73,6 +73,19 @@
</ul> </ul>
<?php endif; ?> <?php endif; ?>
</dd> </dd>
<dt id="watchedFolder-label" class="block-display">
<label class="required" for="watchedFolder"><?php echo $this->element->getElement('watchedFolder')->getLabel() ?></label>
</dt>
<dd id="watchedFolder-element" class="block-display">
<?php echo $this->element->getElement('watchedFolder') ?>
<?php if($this->element->getElement('watchedFolder')->hasErrors()) : ?>
<ul class='errors'>
<?php foreach($this->element->getElement('watchedFolder')->getMessages() as $error): ?>
<li><?php echo $error; ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</dd>
</dl> </dl>
</fieldset> </fieldset>

View File

@ -175,5 +175,5 @@ $(document).ready(function() {
"oLanguage": { "oLanguage": {
"sSearch": "" "sSearch": ""
} }
}); }).fnSetFilteringDelay(350);
}); });

View File

@ -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;
}

View File

@ -14,7 +14,7 @@ echo -e "\n******************************** Install Begin **********************
echo -e "\n*** Creating Pypo User ***" echo -e "\n*** Creating Pypo User ***"
python ${SCRIPTPATH}/../python_apps/create-pypo-user.py python ${SCRIPTPATH}/../python_apps/create-pypo-user.py
php ${SCRIPTPATH}/airtime-install.php $@ php ${SCRIPTPATH}/include/airtime-install.php $@
echo -e "\n*** Pypo Installation ***" echo -e "\n*** Pypo Installation ***"
python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-install.py 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 python ${SCRIPTPATH}/../python_apps/media-monitor/install/media-monitor-install.py
sleep 4 sleep 4
echo -e "\n*** Verifying your system environment ***"
airtime-check-system airtime-check-system
echo -e "\n******************************* Install Complete *******************************" echo -e "\n******************************* Install Complete *******************************"

View File

@ -1,5 +1,9 @@
#!/bin/bash #!/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 # Absolute path to this script
SCRIPT=`readlink -f $0` SCRIPT=`readlink -f $0`
# Absolute directory this script is in # Absolute directory this script is in
@ -7,8 +11,6 @@ SCRIPTPATH=`dirname $SCRIPT`
echo -e "\n******************************* Uninstall Begin ********************************" echo -e "\n******************************* Uninstall Begin ********************************"
php ${SCRIPTPATH}/airtime-uninstall.php
echo -e "\n*** Uninstalling Pypo ***" echo -e "\n*** Uninstalling Pypo ***"
python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-uninstall.py 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 ***" echo -e "\n*** Removing Pypo User ***"
python ${SCRIPTPATH}/../python_apps/remove-pypo-user.py python ${SCRIPTPATH}/../python_apps/remove-pypo-user.py
php ${SCRIPTPATH}/include/airtime-uninstall.php
echo -e "\n****************************** Uninstall Complete ******************************\n" echo -e "\n****************************** Uninstall Complete ******************************\n"
echo "NOTE: To fully remove all Airtime files, you will also have to manually delete" 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)" echo " the directories '/srv/airtime'(default storage location of media files)"

View File

@ -27,6 +27,7 @@ class AirtimeIni
const CONF_FILE_RECORDER = "/etc/airtime/recorder.cfg"; const CONF_FILE_RECORDER = "/etc/airtime/recorder.cfg";
const CONF_FILE_LIQUIDSOAP = "/etc/airtime/liquidsoap.cfg"; const CONF_FILE_LIQUIDSOAP = "/etc/airtime/liquidsoap.cfg";
const CONF_FILE_MEDIAMONITOR = "/etc/airtime/media-monitor.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() public static function IniFilesExist()
{ {
@ -75,10 +76,21 @@ class AirtimeIni
exit(1); exit(1);
} }
if (!copy(__DIR__."/../../python_apps/media-monitor/media-monitor.cfg", AirtimeIni::CONF_FILE_MEDIAMONITOR)){ 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); 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 * 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_PYPO, 'api_key', "'$api_key'");
AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_RECORDER, '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(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) public static function ReadPythonConfig($p_filename)

View File

@ -290,7 +290,7 @@ class AirtimeInstall
public static function DeleteFilesRecursive($p_path) public static function DeleteFilesRecursive($p_path)
{ {
$command = "rm -rf $p_path"; $command = "rm -rf \"$p_path\"";
exec($command); exec($command);
} }
@ -336,7 +336,7 @@ class AirtimeInstall
public static function UninstallPhpCode() public static function UninstallPhpCode()
{ {
echo "* Removing PHP code from ".AirtimeInstall::CONF_DIR_WWW.PHP_EOL; 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() public static function InstallBinaries()
@ -349,7 +349,7 @@ class AirtimeInstall
public static function UninstallBinaries() public static function UninstallBinaries()
{ {
echo "* Removing Airtime binaries from ".AirtimeInstall::CONF_DIR_BINARIES.PHP_EOL; 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() public static function DirCheck()
@ -399,6 +399,6 @@ class AirtimeInstall
$path = AirtimeInstall::CONF_DIR_LOG; $path = AirtimeInstall::CONF_DIR_LOG;
echo "* Removing logs directory ".$path.PHP_EOL; echo "* Removing logs directory ".$path.PHP_EOL;
exec("rm -rf $path"); exec("rm -rf \"$path\"");
} }
} }

View File

@ -8,10 +8,10 @@
* Performs a new install (new configs, database install) if a version of Airtime is not found * 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. * 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__).'/AirtimeIni.php');
require_once(dirname(__FILE__).'/include/AirtimeInstall.php'); require_once(dirname(__FILE__).'/AirtimeInstall.php');
require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/constants.php'); require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/constants.php');
AirtimeInstall::ExitIfNotRoot(); AirtimeInstall::ExitIfNotRoot();
@ -97,6 +97,8 @@ if ($overwrite) {
echo "* Creating INI files".PHP_EOL; echo "* Creating INI files".PHP_EOL;
AirtimeIni::CreateIniFiles(); AirtimeIni::CreateIniFiles();
} }
AirtimeIni::CreateMonitFile();
AirtimeInstall::InstallPhpCode(); AirtimeInstall::InstallPhpCode();
AirtimeInstall::InstallBinaries(); AirtimeInstall::InstallBinaries();
@ -106,6 +108,9 @@ if ($overwrite) {
AirtimeIni::UpdateIniFiles(); 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'); require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/conf.php');
echo "* Airtime Version: ".AIRTIME_VERSION.PHP_EOL; echo "* Airtime Version: ".AIRTIME_VERSION.PHP_EOL;

View File

@ -5,8 +5,8 @@
* @license http://www.gnu.org/licenses/gpl.txt * @license http://www.gnu.org/licenses/gpl.txt
*/ */
require_once(dirname(__FILE__).'/include/AirtimeIni.php'); require_once(dirname(__FILE__).'/AirtimeIni.php');
require_once(dirname(__FILE__).'/include/AirtimeInstall.php'); require_once(dirname(__FILE__).'/AirtimeInstall.php');
// Need to check that we are superuser before running this. // Need to check that we are superuser before running this.
AirtimeInstall::ExitIfNotRoot(); AirtimeInstall::ExitIfNotRoot();
@ -69,7 +69,7 @@ if ($dbDeleteFailed) {
// Delete the user // Delete the user
//------------------------------------------------------------------------ //------------------------------------------------------------------------
echo " * Deleting database user '{$CC_CONFIG['dsn']['username']}'...".PHP_EOL; 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); @exec($command, $output, $results);
if ($results == 0) { if ($results == 0) {
echo " * User '{$CC_CONFIG['dsn']['username']}' deleted.".PHP_EOL; echo " * User '{$CC_CONFIG['dsn']['username']}' deleted.".PHP_EOL;
@ -88,6 +88,7 @@ if ($results == 0) {
AirtimeInstall::RemoveSymlinks(); AirtimeInstall::RemoveSymlinks();
AirtimeInstall::UninstallBinaries(); AirtimeInstall::UninstallBinaries();
AirtimeInstall::RemoveLogDirectories(); AirtimeInstall::RemoveLogDirectories();
AirtimeIni::RemoveMonitFile();
unlink('/etc/cron.d/airtime-crons'); unlink('/etc/cron.d/airtime-crons');

View File

@ -7,8 +7,9 @@
*/ */
//Pear classes. //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('DB.php');
require_once(dirname(__FILE__).'/AirtimeIni.php');
if(exec("whoami") != "root"){ if(exec("whoami") != "root"){
echo "Must be root user.\n"; echo "Must be root user.\n";
@ -67,19 +68,19 @@ echo "******************************** Update Begin ****************************
$version = substr($version, 0, 5); $version = substr($version, 0, 5);
if (strcmp($version, "1.7.0") < 0){ 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){ 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){ 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){ 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){ 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; 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; 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; echo PHP_EOL."*** Updating Media Monitor ***".PHP_EOL;
passthru("python ".__DIR__."/../python_apps/media-monitor/install/media-monitor-install.py"); passthru("python ".__DIR__."/../../python_apps/media-monitor/install/media-monitor-install.py");
AirtimeIni::CreateMonitFile();
echo "******************************* Update Complete *******************************".PHP_EOL; echo "******************************* Update Complete *******************************".PHP_EOL;

View File

@ -39,7 +39,7 @@ function InstallBinaries()
function UninstallBinaries() function UninstallBinaries()
{ {
echo "* Removing Airtime binaries from ".CONF_DIR_BINARIES.PHP_EOL; 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){ foreach ($pathnames as $pn){
echo "Removing $pn\n"; echo "Removing $pn\n";
exec("rm -rf ".$pn); exec("rm -rf \"$pn\"");
} }

View File

@ -31,24 +31,6 @@ def api_client_factory(config):
logger.info('API Client "'+config["api_client"]+'" not supported. Please check your config file.\n') logger.info('API Client "'+config["api_client"]+'" not supported. Please check your config file.\n')
sys.exit() 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: class ApiClientInterface:
# Implementation: optional # Implementation: optional
@ -402,11 +384,12 @@ class AirTimeApiClient(ApiClientInterface):
response = None response = None
try: 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"]) 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("%%api_key%%", self.config["api_key"])
url = url.replace("%%mode%%", mode) url = url.replace("%%mode%%", mode)
logger.debug(url)
data = recursive_urlencode(md) data = urllib.urlencode(md)
req = urllib2.Request(url, data) req = urllib2.Request(url, data)
response = urllib2.urlopen(req).read() response = urllib2.urlopen(req).read()
@ -636,7 +619,7 @@ class ObpApiClient():
def get_liquidsoap_data(self, pkey, schedule): def get_liquidsoap_data(self, pkey, schedule):
playlist = schedule[pkey] playlist = schedule[pkey]
data = dict() data = dict()
data["ptype"] = playlist['subtype'] #data["ptype"] = playlist['subtype']
try: try:
data["user_id"] = playlist['user_id'] data["user_id"] = playlist['user_id']
data["playlist_id"] = playlist['id'] data["playlist_id"] = playlist['id']

View File

@ -1,4 +1,5 @@
import os import os
import sys
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
def create_user(username): def create_user(username):

View File

@ -10,9 +10,12 @@ import hashlib
import json import json
import shutil import shutil
import math import math
import socket
import grp
import pwd
from collections import deque from collections import deque
from pwd import getpwnam
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
from configobj import ConfigObj from configobj import ConfigObj
@ -26,6 +29,8 @@ from kombu.connection import BrokerConnection
from kombu.messaging import Exchange, Queue, Consumer, Producer from kombu.messaging import Exchange, Queue, Consumer, Producer
from api_clients import api_client from api_clients import api_client
from multiprocessing import Process, Lock
MODE_CREATE = "create" MODE_CREATE = "create"
MODE_MODIFY = "modify" MODE_MODIFY = "modify"
MODE_MOVED = "moved" 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'] ['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): def __init__(self):
Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout)
self.airtime2mutagen = {\ self.airtime2mutagen = {\
"MDATA_KEY_TITLE": "title",\ "MDATA_KEY_TITLE": "title",\
@ -77,50 +81,6 @@ class AirtimeNotifier(Notifier):
"MDATA_KEY_COPYRIGHT": "copyright",\ "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 = {\ self.mutagen2airtime = {\
"title": "MDATA_KEY_TITLE",\ "title": "MDATA_KEY_TITLE",\
"artist": "MDATA_KEY_CREATOR",\ "artist": "MDATA_KEY_CREATOR",\
@ -139,26 +99,7 @@ class MediaMonitor(ProcessEvent):
"copyright": "MDATA_KEY_COPYRIGHT",\ "copyright": "MDATA_KEY_COPYRIGHT",\
} }
self.supported_file_formats = ['mp3', 'ogg']
self.logger = logging.getLogger('root') 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): def get_md5(self, filepath):
f = open(filepath, 'rb') f = open(filepath, 'rb')
@ -185,6 +126,192 @@ class MediaMonitor(ProcessEvent):
return length 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): def ensure_dir(self, filepath):
directory = os.path.dirname(filepath) directory = os.path.dirname(filepath)
@ -196,21 +323,38 @@ class MediaMonitor(ProcessEvent):
finally: finally:
os.umask(omask) 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): def create_unique_filename(self, filepath):
if(os.path.exists(filepath)): try:
file_dir = os.path.dirname(filepath) if(os.path.exists(filepath)):
filename = os.path.basename(filepath).split(".")[0] self.logger.info("Path %s exists", filepath)
#will be in the format .ext file_dir = os.path.dirname(filepath)
file_ext = os.path.splitext(filepath)[1] filename = os.path.basename(filepath).split(".")[0]
i = 1; #will be in the format .ext
while(True): file_ext = os.path.splitext(filepath)[1]
new_filepath = "%s/%s(%s)%s" % (file_dir, filename, i, file_ext) 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)): if(os.path.exists(new_filepath)):
i = i+1; i = i+1;
else: else:
filepath = new_filepath filepath = new_filepath
break
except Exception, e:
self.logger.error("Exception %s", e)
return filepath return filepath
@ -226,23 +370,39 @@ class MediaMonitor(ProcessEvent):
#will be in the format .ext #will be in the format .ext
file_ext = os.path.splitext(imported_filepath)[1] 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'] 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: for m in path_md:
if m not in 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 filepath = None
if (md['MDATA_KEY_TITLE'] == 'unknown'): if (md['MDATA_KEY_TITLE'] == u'unknown'.encode('utf-8')):
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) self.logger.info('unknown title')
elif(md['MDATA_KEY_TRACKNUMBER'] == 'unknown'): 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)
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) 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: 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) filepath = self.create_unique_filename(filepath)
self.logger.info(u'Unique filepath: %s', filepath)
self.ensure_dir(filepath) self.ensure_dir(filepath)
except Exception, e: except Exception, e:
@ -250,63 +410,6 @@ class MediaMonitor(ProcessEvent):
return filepath 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): def is_temp_file(self, filename):
info = filename.split(".") info = filename.split(".")
@ -334,18 +437,20 @@ class MediaMonitor(ProcessEvent):
global plupload_directory global plupload_directory
#files that have been added through plupload have a placeholder already put in Airtime's database. #files that have been added through plupload have a placeholder already put in Airtime's database.
if not self.is_parent_directory(event.pathname, plupload_directory): if not self.is_parent_directory(event.pathname, plupload_directory):
md5 = self.get_md5(event.pathname) if self.is_audio_file(event.pathname):
response = self.api_client.check_media_status(md5) 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. #this file is new, md5 does not exist in Airtime.
if(response['airtime_status'] == 0): if(response['airtime_status'] == 0):
filepath = self.create_file_path(event.pathname) filepath = self.create_file_path(event.pathname)
os.rename(event.pathname, filepath) self.move_file(event.pathname, filepath)
self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath}) self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath})
#immediately add a watch on the new directory.
else: else:
self.watch_directory(event.pathname) self.set_needed_file_permissions(event.pathname, event.dir)
def process_IN_MODIFY(self, event): def process_IN_MODIFY(self, event):
if not event.dir: if not event.dir:
@ -367,6 +472,8 @@ class MediaMonitor(ProcessEvent):
def process_IN_MOVED_TO(self, event): def process_IN_MOVED_TO(self, event):
self.logger.info("%s: %s", event.maskname, event.pathname) 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 not event.dir:
if event.cookie in self.temp_files: if event.cookie in self.temp_files:
del self.temp_files[event.cookie] 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. #file renamed from /tmp/plupload does not have a path in our naming scheme yet.
md_filepath = self.create_file_path(event.pathname) md_filepath = self.create_file_path(event.pathname)
#move the file a second time to its correct Airtime naming schema. #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}) self.file_events.append({'filepath': md_filepath, 'mode': MODE_MOVED})
else: else:
self.file_events.append({'filepath': event.pathname, 'mode': MODE_MOVED}) 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. #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) #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) 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}) self.file_events.append({'mode': MODE_CREATE, 'filepath': md_filepath})
def process_IN_DELETE(self, event): def process_IN_DELETE(self, event):
@ -402,12 +509,22 @@ class MediaMonitor(ProcessEvent):
def notifier_loop_callback(self, notifier): 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: while len(self.file_events) > 0:
self.logger.info("Processing a file event update to Airtime.")
file_info = self.file_events.popleft() file_info = self.file_events.popleft()
self.update_airtime(file_info) notifier.update_airtime(file_info)
try: try:
notifier.connection.drain_events(timeout=1) notifier.connection.drain_events(timeout=1)
#avoid logging a bunch of timeout messages.
except socket.timeout:
pass
except Exception, e: except Exception, e:
self.logger.info("%s", e) self.logger.info("%s", e)

View File

@ -9,19 +9,21 @@
# Short-Description: Manage airtime-media-monitor daemon # Short-Description: Manage airtime-media-monitor daemon
### END INIT INFO ### END INIT INFO
USERID=pypo USERID=root
GROUPID=pypo GROUPID=www-data
NAME=Airtime 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 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 start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE --startas $DAEMON
} }
stop () { stop () {
# Send TERM after 5 seconds, wait at most 30 seconds. # 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 start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE
rm -f $PIDFILE rm -f $PIDFILE
} }

View File

@ -34,7 +34,7 @@ def copy_dir(src_dir, dest_dir):
if not (os.path.exists(dest_dir)): if not (os.path.exists(dest_dir)):
print "Copying directory "+os.path.realpath(src_dir)+" to "+os.path.realpath(dest_dir) print "Copying directory "+os.path.realpath(src_dir)+" to "+os.path.realpath(dest_dir)
shutil.copytree(src_dir, dest_dir) shutil.copytree(src_dir, dest_dir)
def get_current_script_dir(): def get_current_script_dir():
current_script_dir = os.path.realpath(__file__) current_script_dir = os.path.realpath(__file__)
index = current_script_dir.rindex('/') index = current_script_dir.rindex('/')
@ -60,21 +60,18 @@ try:
os.system("chown -R pypo:pypo "+config["log_dir"]) os.system("chown -R pypo:pypo "+config["log_dir"])
copy_dir("%s/.."%current_script_dir, config["bin_dir"]) copy_dir("%s/.."%current_script_dir, config["bin_dir"])
print "Setting permissions" print "Setting permissions"
os.system("chmod -R 755 "+config["bin_dir"]) 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"]) 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" print "Installing media-monitor daemon"
shutil.copy(config["bin_dir"]+"/airtime-media-monitor-init-d", "/etc/init.d/airtime-media-monitor") 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] sts = os.waitpid(p.pid, 0)[1]
print "Waiting for processes to start..." print "Waiting for processes to start..."
p = Popen("/etc/init.d/airtime-media-monitor start", shell=True) p = Popen("/etc/init.d/airtime-media-monitor start", shell=True)
sts = os.waitpid(p.pid, 0)[1] sts = os.waitpid(p.pid, 0)[1]

View File

@ -12,7 +12,7 @@ if os.geteuid() != 0:
PATH_INI_FILE = '/etc/airtime/media-monitor.cfg' PATH_INI_FILE = '/etc/airtime/media-monitor.cfg'
def remove_path(path): def remove_path(path):
os.system("rm -rf " + path) os.system('rm -rf "%s"' % path)
def get_current_script_dir(): def get_current_script_dir():
current_script_dir = os.path.realpath(__file__) current_script_dir = os.path.realpath(__file__)
@ -29,7 +29,7 @@ try:
os.system("/etc/init.d/airtime-media-monitor stop") os.system("/etc/init.d/airtime-media-monitor stop")
os.system("rm -f /etc/init.d/airtime-media-monitor") 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" print "Removing log directories"
remove_path(config["log_dir"]) remove_path(config["log_dir"])

View File

@ -1,5 +1,10 @@
set daemon 10 # Poll at 10 second intervals set daemon 10 # Poll at 10 second intervals
set logfile syslog facility log_daemon 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 check process airtime-playout
with pidfile "/var/run/airtime-playout.pid" with pidfile "/var/run/airtime-playout.pid"
start program = "/etc/init.d/airtime-playout start" with timeout 10 seconds start program = "/etc/init.d/airtime-playout start" with timeout 10 seconds

View File

@ -11,24 +11,31 @@
USERID=pypo USERID=pypo
GROUPID=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 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 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 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 start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE1 --startas $DAEMON1
} }
stop () { stop () {
# Send TERM after 5 seconds, wait at most 30 seconds. # 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 $PIDFILE0
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE1
rm -f $PIDFILE0 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 rm -f $PIDFILE1
} }

View File

@ -81,10 +81,10 @@ try:
if architecture == '64bit' and natty: if architecture == '64bit' and natty:
print "Installing 64-bit liquidsoap binary (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: elif architecture == '32bit' and natty:
print "Installing 32-bit liquidsoap binary (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: elif architecture == '64bit' and not natty:
print "Installing 64-bit liquidsoap binary" print "Installing 64-bit liquidsoap binary"
shutil.copy("%s/../liquidsoap_bin/liquidsoap-amd64"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir) 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["bin_dir"])
os.system("chown -R pypo:pypo "+config["cache_base_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" print "Installing pypo daemon"
shutil.copy(config["bin_dir"]+"/bin/airtime-playout-init-d", "/etc/init.d/airtime-playout") 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] sts = os.waitpid(p.pid, 0)[1]
print "Waiting for processes to start..." print "Waiting for processes to start..."

View File

@ -12,7 +12,7 @@ if os.geteuid() != 0:
PATH_INI_FILE = '/etc/airtime/pypo.cfg' PATH_INI_FILE = '/etc/airtime/pypo.cfg'
def remove_path(path): def remove_path(path):
os.system("rm -rf " + path) os.system('rm -rf "%s"' % path)
def get_current_script_dir(): def get_current_script_dir():
current_script_dir = os.path.realpath(__file__) current_script_dir = os.path.realpath(__file__)
@ -29,7 +29,7 @@ try:
os.system("/etc/init.d/airtime-playout stop") os.system("/etc/init.d/airtime-playout stop")
os.system("rm -f /etc/init.d/airtime-playout") 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" print "Removing cache directories"
remove_path(config["cache_base_dir"]) remove_path(config["cache_base_dir"])

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,5 @@
SUBDIRS = tests
DISTFILES = $(wildcard *.in) Makefile ask-liquidsoap.rb ask-liquidsoap.pl \ DISTFILES = $(wildcard *.in) Makefile ask-liquidsoap.rb ask-liquidsoap.pl \
$(wildcard *.liq) extract-replaygain $(wildcard *.liq) extract-replaygain

View File

@ -9,37 +9,43 @@ my_get_mime = get_mime
get_mime = my_get_mime get_mime = my_get_mime
%ifdef add_decoder %ifdef add_decoder
if test_process("which flac") then # Enable external FLAC decoders. Requires flac binary
log(level=3,"Found flac binary: enabling flac external decoder.") # in the path for audio decoding and metaflac binary for
flac_p = "flac -d -c - 2>/dev/null" # metadata. Does not work on Win32. Default: disabled.
def test_flac(file) = # Please note that built-in support for FLAC is available
if test_process("which metaflac") then # in liquidsoap if compiled and should be preferred over
channels = list.hd(get_process_lines("metaflac \ # the external decoder.
--show-channels #{quote(file)} \ # @category Liquidsoap
2>/dev/null")) def enable_external_flac_decoder() =
# If the value is not an int, this returns 0 and we are ok :) if test_process("which flac") then
int_of_string(channels) log(level=3,"Found flac binary: enabling flac external decoder.")
else flac_p = "flac -d -c - 2>/dev/null"
# Try to detect using mime test.. def test_flac(file) =
mime = get_mime(file) if test_process("which metaflac") then
if string.match(pattern="flac",file) then channels = list.hd(get_process_lines("metaflac \
# We do not know the number of audio channels --show-channels #{quote(file)} \
# so setting to -1 2>/dev/null"))
(-1) # If the value is not an int, this returns 0 and we are ok :)
int_of_string(channels)
else else
# All tests failed: no audio decodable using flac.. # Try to detect using mime test..
0 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
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 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 if test_process("which metaflac") then
log(level=3,"Found metaflac binary: enabling flac external metadata \ log(level=3,"Found metaflac binary: enabling flac external metadata \
resolver.") resolver.")
@ -55,49 +61,59 @@ if os.type != "Win32" then
if list.length(l) >= 1 then if list.length(l) >= 1 then
list.append([(list.hd(l),"")],l') list.append([(list.hd(l),"")],l')
else else
l' l'
end
end end
end end
list.fold(f,[],ret) end
list.fold(f,[],ret)
end end
add_metadata_resolver("FLAC",flac_meta) add_metadata_resolver("FLAC",flac_meta)
else 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
end end
%endif
# A list of know extensions and content-type for AAC. %ifdef add_oblivious_decoder
# Values from http://en.wikipedia.org/wiki/Advanced_Audio_Coding # Enable or disable external FAAD (AAC/AAC+/M4A) decoders.
# TODO: can we register a setting for that ?? # Requires faad binary in the path for audio decoding and
aac_mimes = # metaflac binary for metadata. Does not work on Win32.
["audio/aac", "audio/aacp", "audio/3gpp", "audio/3gpp2", "audio/mp4", # Please note that built-in support for faad is available
"audio/MP4A-LATM", "audio/mpeg4-generic", "audio/x-hx-aac-adts"] # in liquidsoap if compiled and should be preferred over
aac_filexts = ["m4a", "m4b", "m4p", "m4v", # the external decoder.
"m4r", "3gp", "mp4", "aac"] # @category Liquidsoap
def enable_external_faad_decoder() =
# Faad is not very selective so # A list of know extensions and content-type for AAC.
# We are checking only file that # Values from http://en.wikipedia.org/wiki/Advanced_Audio_Coding
# end with a known extension or mime type # TODO: can we register a setting for that ??
def faad_test(file) = aac_mimes =
# Get the file's mime ["audio/aac", "audio/aacp", "audio/3gpp", "audio/3gpp2", "audio/mp4",
mime = get_mime(file) "audio/MP4A-LATM", "audio/mpeg4-generic", "audio/x-hx-aac-adts"]
# Test mime aac_filexts = ["m4a", "m4b", "m4p", "m4v",
if list.mem(mime,aac_mimes) then "m4r", "3gp", "mp4", "aac"]
true
else # Faad is not very selective so
# Otherwise test file extension # We are checking only file that
ret = string.extract(pattern='\.(.+)$',file) # 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 if list.length(ret) != 0 then
ext = ret["1"] ext = ret["1"]
list.mem(ext,aac_filexts) list.mem(ext,aac_filexts)
else else
false false
end end
end
end end
end
if os.type != "Win32" then
if test_process("which faad") then if test_process("which faad") then
log(level=3,"Found faad binary: enabling external faad decoder and \ log(level=3,"Found faad binary: enabling external faad decoder and \
metadata resolver.") metadata resolver.")
@ -120,15 +136,13 @@ if os.type != "Win32" then
0 0
end end
end end
%ifdef add_oblivious_decoder
add_oblivious_decoder(name="FAAD",description="Decode files using \ add_oblivious_decoder(name="FAAD",description="Decode files using \
the faad binary.", test=test_faad, faad_p) the faad binary.", test=test_faad, faad_p)
%endif
def faad_meta(file) = def faad_meta(file) =
if faad_test(file) then if faad_test(file) then
ret = get_process_lines("faad -i \ ret = get_process_lines("faad -i \
#{quote(file)} 2>&1") #{quote(file)} 2>&1")
# Yea, this is tuff programming (again) ! # Yea, this is ugly programming (again) !
def get_meta(l,s)= def get_meta(l,s)=
ret = string.extract(pattern="^(\w+):\s(.+)$",s) ret = string.extract(pattern="^(\w+):\s(.+)$",s)
if list.length(ret) > 0 then 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.") log(level=3,"Did not find faad binary: faad decoder disabled.")
end end
end end
%endif
# Standard function for displaying metadata. # Standard function for displaying metadata.
# Shows artist and title, using "Unknown" when a field is empty. # 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)} ' ^ ' -t #{time} #{quote(title)} '
on_metadata(fun (m) -> system(send^quote(display(m))),s) on_metadata(fun (m) -> system(send^quote(display(m))),s)
end 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

View File

View File

@ -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))

View File

@ -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

View File

@ -300,6 +300,25 @@ def server.insert_metadata(~id="",s) =
s s
end 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. # Get the base name of a path.
# Implemented using the corresponding shell command. # Implemented using the corresponding shell command.
# @category System # @category System
@ -479,59 +498,95 @@ def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
end end
# Custom playlist source written using the script language. # 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 # @category Source / Input
# @param ~id Force the value of the source ID.
# @param ~random Randomize playlist content # @param ~random Randomize playlist content
# @param ~on_done Function to execute when the playlist is finished # @param ~on_done Function to execute when the playlist is finished
# @param uri Playlist URI # @param uri Playlist URI
def playlist.once(~random=false,~on_done={()},uri) def playlist.reloadable(~id="",~random=false,~on_done={()},uri)
x = ref 0 # A reference to the playlist
def playlist.custom(files) playlist = ref []
length = list.length(files) # A reference to the uri
if length == 0 then playlist_uri = ref uri
log("Empty playlist..") # A reference to know if the source
fail () # has been stopped
else has_stopped = ref false
files = # The next function
if random then def next () =
list.sort(fun (x,y) -> int_of_float(random.float()), files) file =
else if list.length(!playlist) > 0 then
files ret = list.hd(!playlist)
end playlist := list.tl(!playlist)
def next () = ret
state = !x else
file = # Playlist finished
if state < length then if not !has_stopped then
x := state + 1
list.nth(files,state)
else
# Playlist finished
on_done () on_done ()
""
end end
request.create(file) has_stopped := true
""
end 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
end end
if test_process("test -d #{quote(uri)}") then # The reload function
files = get_process_lines("find #{quote(uri)} -type f | sort") def reload(~uri="") =
playlist.custom(files) if uri != "" then
else playlist_uri := uri
playlist = request.create.raw(uri) end
result = log(label=id,"Reloading playlist with URI #{!playlist_uri}")
if request.resolve(playlist) then has_stopped := false
playlist = request.filename(playlist) load_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
end 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 end
# Mixes two streams, with faded transitions between the state when only the # 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) add_timeout(freq,check)
end end
# Register the replaygain protocol # Register the replaygain protocol.
# @category Liquidsoap
def replaygain_protocol(arg,delay) def replaygain_protocol(arg,delay)
# The extraction program # The extraction program
extract_replaygain = "#{configure.libdir}/extract-replaygain" extract_replaygain = "#{configure.libdir}/extract-replaygain"

View File

@ -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/<script>.log" log_file = "/var/log/airtime/pypo-liquidsoap/<script>.log"
log_level = 3 log_level = 3
########################################### ###########################################
# stream settings # # Icecast Stream settings #
########################################### ###########################################
icecast_host = "127.0.0.1" icecast_host = "127.0.0.1"
icecast_port = 8000 icecast_port = 8000
icecast_pass = "hackme" icecast_pass = "hackme"
########################################### # Icecast mountpoint names
# webstream mountpoint names #
###########################################
mount_point_mp3 = "airtime.mp3" mount_point_mp3 = "airtime.mp3"
mount_point_vorbis = "airtime.ogg" mount_point_vorbis = "airtime.ogg"
########################################### # Webstream metadata settings
# webstream metadata settings #
###########################################
icecast_url = "http://airtime.sourcefabric.org" icecast_url = "http://airtime.sourcefabric.org"
icecast_description = "Airtime Radio!" icecast_description = "Airtime Radio!"
icecast_genre = "genre" icecast_genre = "genre"
########################################### # Audio stream metadata for vorbis/ogg is disabled by default
#liquidsoap output settings # # due to a number of client media players that disconnect
########################################### # when the metadata changes to a new track. Some versions of
output_sound_device = false # mplayer and VLC have this problem. Enable this option at your
output_icecast_vorbis = true # own risk!
output_icecast_mp3 = false
#audio stream metadata for vorbis/ogg is disabled by default
#due to a large number of client media players that disconnect
#when the metadata changes to that of a new track. Some versions of
#mplayer and VLC have this problem. Enable this option at your
#own risk!
output_icecast_vorbis_metadata = false output_icecast_vorbis_metadata = false
###########################################
# Shoutcast Stream settings #
###########################################
shoutcast_host = "127.0.0.1"
shoutcast_port = 9000
shoutcast_pass = "testing"
# Webstream metadata settings
shoutcast_url = "http://airtime.sourcefabric.org"
shoutcast_genre = "genre"

View File

@ -7,6 +7,7 @@ set("server.telnet", true)
set("server.telnet.port", 1234) set("server.telnet.port", 1234)
queue = request.queue(id="queue", length=0.5) queue = request.queue(id="queue", length=0.5)
queue = cue_cut(queue)
queue = audio_to_stereo(queue) queue = audio_to_stereo(queue)
pypo_data = ref '0' pypo_data = ref '0'
@ -57,8 +58,10 @@ end
if output_icecast_mp3 then if output_icecast_mp3 then
out_mp3 = output.icecast(%mp3, out_mp3 = output.icecast(%mp3,
host = icecast_host, port = icecast_port, host = icecast_host,
password = icecast_pass, mount = mount_point_mp3, port = icecast_port,
password = icecast_pass,
mount = mount_point_mp3,
fallible = true, fallible = true,
restart = true, restart = true,
restart_delay = 5, restart_delay = 5,
@ -69,11 +72,12 @@ if output_icecast_mp3 then
end end
if output_icecast_vorbis then if output_icecast_vorbis then
if output_icecast_vorbis_metadata then if output_icecast_vorbis_metadata then
out_vorbis = output.icecast(%vorbis, out_vorbis = output.icecast(%vorbis,
host = icecast_host, port = icecast_port, host = icecast_host,
password = icecast_pass, mount = mount_point_vorbis, port = icecast_port,
password = icecast_pass,
mount = mount_point_vorbis,
fallible = true, fallible = true,
restart = true, restart = true,
restart_delay = 5, restart_delay = 5,
@ -86,16 +90,30 @@ if output_icecast_vorbis then
#with vlc and mplayer disconnecting at the end of every track #with vlc and mplayer disconnecting at the end of every track
s = add(normalize=false, [amplify(0.00001, noise()),s]) s = add(normalize=false, [amplify(0.00001, noise()),s])
out_vorbis = output.icecast(%vorbis, out_vorbis = output.icecast(%vorbis,
host = icecast_host, port = icecast_port, host = icecast_host,
password = icecast_pass, mount = mount_point_vorbis, port = icecast_port,
password = icecast_pass,
mount = mount_point_vorbis,
fallible = true, fallible = true,
restart = true, restart = true,
restart_delay = 5, restart_delay = 5,
url = icecast_url, url = icecast_url,
description = icecast_description, description = icecast_description,
genre = icecast_genre, genre = icecast_genre,
s) s)
end end
end end
if output_shoutcast then
out_shoutcast = output.shoutcast(%mp3,
host = shoutcast_host,
port = shoutcast_port,
password = shoutcast_pass,
fallible = true,
restart = true,
restart_delay = 5,
url = shoutcast_url,
genre = shoutcast_genre,
s)
end

View File

@ -90,7 +90,6 @@ class Global:
print '*****************************************' print '*****************************************'
print '\033[0;32m%s %s\033[m' % ('scheduled at:', str(pkey)) print '\033[0;32m%s %s\033[m' % ('scheduled at:', str(pkey))
print 'cached at : ' + self.cache_dir + str(pkey) print 'cached at : ' + self.cache_dir + str(pkey)
print 'subtype: ' + str(playlist['subtype'])
print 'played: ' + str(playlist['played']) print 'played: ' + str(playlist['played'])
print 'schedule id: ' + str(playlist['schedule_id']) print 'schedule id: ' + str(playlist['schedule_id'])
print 'duration: ' + str(playlist['duration']) print 'duration: ' + str(playlist['duration'])

View File

@ -37,7 +37,6 @@ from configobj import ConfigObj
# custom imports # custom imports
from util import * from util import *
from api_clients import * from api_clients import *
from dls import *
# Set up command-line options # Set up command-line options
parser = OptionParser() parser = OptionParser()

View File

@ -4,7 +4,6 @@ import time
import logging import logging
import logging.config import logging.config
import shutil import shutil
import pickle
import random import random
import string import string
import json import json
@ -12,6 +11,8 @@ import telnetlib
import math import math
from threading import Thread from threading import Thread
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from datetime import datetime
from datetime import timedelta
# For RabbitMQ # For RabbitMQ
from kombu.connection import BrokerConnection from kombu.connection import BrokerConnection
@ -98,19 +99,59 @@ class PypoFetch(Thread):
logger.error(" * To fix this, you need to set the 'date.timezone' value in your php.ini file and restart apache.") logger.error(" * To fix this, you need to set the 'date.timezone' value in your php.ini file and restart apache.")
logger.error(" * See this page for more info (v1.7): http://wiki.sourcefabric.org/x/BQBF") logger.error(" * See this page for more info (v1.7): http://wiki.sourcefabric.org/x/BQBF")
logger.error(" * and also the 'FAQ and Support' page underneath it.") logger.error(" * and also the 'FAQ and Support' page underneath it.")
"""
def get_currently_scheduled(self, playlistsOrMedias, str_tnow_s):
for key in playlistsOrMedias:
start = playlistsOrMedias[key]['start']
end = playlistsOrMedias[key]['end']
if start <= str_tnow_s and str_tnow_s < end:
return key
return None
def handle_shows_currently_scheduled(self, playlists):
logger = logging.getLogger('fetch')
dtnow = datetime.today()
tnow = dtnow.timetuple()
str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5])
current_pkey = self.get_currently_scheduled(playlists, str_tnow_s)
if current_pkey is not None:
logger.debug("FOUND CURRENT PLAYLIST %s", current_pkey)
# So we have found that a playlist if currently scheduled
# even though we just started pypo. Perhaps there was a
# system crash. Lets calculate what position in the playlist
# we are supposed to be in.
medias = playlists[current_pkey]["medias"]
current_mkey = self.get_currently_scheduled(medias, str_tnow_s)
if current_mkey is not None:
mkey_split = map(int, current_mkey.split('-'))
media_start = datetime(mkey_split[0], mkey_split[1], mkey_split[2], mkey_split[3], mkey_split[4], mkey_split[5])
logger.debug("Found media item that started at %s.", media_start)
delta = dtnow - media_start #we get a TimeDelta object from this operation
logger.info("Starting media item at %d second point", delta.seconds)
"""
""" """
Process the schedule Process the schedule
- Reads the scheduled entries of a given range (actual time +/- "prepare_ahead" / "cache_for") - Reads the scheduled entries of a given range (actual time +/- "prepare_ahead" / "cache_for")
- Saves a serialized file of the schedule - Saves a serialized file of the schedule
- playlists are prepared. (brought to liquidsoap format) and, if not mounted via nsf, files are copied - playlists are prepared. (brought to liquidsoap format) and, if not mounted via nsf, files are copied
to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss) to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss)
- runs the cleanup routine, to get rid of unused cashed files - runs the cleanup routine, to get rid of unused cached files
""" """
def process_schedule(self, schedule_data, export_source): def process_schedule(self, schedule_data, export_source, bootstrapping):
logger = logging.getLogger('fetch') logger = logging.getLogger('fetch')
playlists = schedule_data["playlists"] playlists = schedule_data["playlists"]
#if bootstrapping:
#TODO: possible allow prepare_playlists to handle this.
#self.handle_shows_currently_scheduled(playlists)
self.check_matching_timezones(schedule_data["server_timezone"]) self.check_matching_timezones(schedule_data["server_timezone"])
# Push stream metadata to liquidsoap # Push stream metadata to liquidsoap
@ -127,9 +168,9 @@ class PypoFetch(Thread):
logger.error("Exception %s", e) logger.error("Exception %s", e)
status = 0 status = 0
# Download all the media and put playlists in liquidsoap format # Download all the media and put playlists in liquidsoap "annotate" format
try: try:
liquidsoap_playlists = self.prepare_playlists(playlists) liquidsoap_playlists = self.prepare_playlists(playlists, bootstrapping)
except Exception, e: logger.error("%s", e) except Exception, e: logger.error("%s", e)
# Send the data to pypo-push # Send the data to pypo-push
@ -149,7 +190,7 @@ class PypoFetch(Thread):
and stored in a playlist folder. and stored in a playlist folder.
file is e.g. 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3 file is e.g. 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3
""" """
def prepare_playlists(self, playlists): def prepare_playlists(self, playlists, bootstrapping):
logger = logging.getLogger('fetch') logger = logging.getLogger('fetch')
liquidsoap_playlists = dict() liquidsoap_playlists = dict()
@ -170,27 +211,18 @@ class PypoFetch(Thread):
try: try:
os.mkdir(self.cache_dir + str(pkey)) os.mkdir(self.cache_dir + str(pkey))
except Exception, e: except Exception, e:
pass logger.error(e)
#logger.debug('*****************************************') #June 13, 2011: Commented this block out since we are not currently setting this to '1'
#logger.debug('pkey: ' + str(pkey)) #on the server side. Currently using a different method to detect if already played - Martin
#logger.debug('cached at : ' + self.cache_dir + str(pkey)) #if int(playlist['played']) == 1:
#logger.debug('subtype: ' + str(playlist['subtype'])) # logger.info("playlist %s already played / sent to liquidsoap, so will ignore it", pkey)
#logger.debug('played: ' + str(playlist['played']))
#logger.debug('schedule id: ' + str(playlist['schedule_id'])) ls_playlist = self.handle_media_file(playlist, pkey, bootstrapping)
#logger.debug('duration: ' + str(playlist['duration']))
#logger.debug('source id: ' + str(playlist['x_ident']))
#logger.debug('*****************************************')
if int(playlist['played']) == 1:
logger.info("playlist %s already played / sent to liquidsoap, so will ignore it", pkey)
elif int(playlist['subtype']) > 0 and int(playlist['subtype']) < 5:
ls_playlist = self.handle_media_file(playlist, pkey)
liquidsoap_playlists[pkey] = ls_playlist liquidsoap_playlists[pkey] = ls_playlist
except Exception, e: except Exception, e:
logger.info("%s", e) logger.error("%s", e)
return liquidsoap_playlists return liquidsoap_playlists
@ -199,27 +231,47 @@ class PypoFetch(Thread):
This handles both remote and local files. This handles both remote and local files.
Returns an updated ls_playlist string. Returns an updated ls_playlist string.
""" """
def handle_media_file(self, playlist, pkey): def handle_media_file(self, playlist, pkey, bootstrapping):
ls_playlist = []
logger = logging.getLogger('fetch') logger = logging.getLogger('fetch')
for media in playlist['medias']:
ls_playlist = []
dtnow = datetime.today()
str_tnow_s = dtnow.strftime('%Y-%m-%d-%H-%M-%S')
sortedKeys = sorted(playlist['medias'].iterkeys())
for key in sortedKeys:
media = playlist['medias'][key]
logger.debug("Processing track %s", media['uri']) logger.debug("Processing track %s", media['uri'])
if bootstrapping:
start = media['start']
end = media['end']
if end <= str_tnow_s:
continue
elif start <= str_tnow_s and str_tnow_s < end:
#song is currently playing and we just started pypo. Maybe there
#was a power outage? Let's restart playback of this song.
start_split = map(int, start.split('-'))
media_start = datetime(start_split[0], start_split[1], start_split[2], start_split[3], start_split[4], start_split[5])
logger.debug("Found media item that started at %s.", media_start)
delta = dtnow - media_start #we get a TimeDelta object from this operation
logger.info("Starting media item at %d second point", delta.seconds)
media['cue_in'] = delta.seconds + 10
td = timedelta(seconds=10)
playlist['start'] = (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S')
logger.info("Crash detected, setting playlist to restart at %s", (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S'))
fileExt = os.path.splitext(media['uri'])[1] fileExt = os.path.splitext(media['uri'])[1]
try: try:
if str(media['cue_in']) == '0' and str(media['cue_out']) == '0': dst = "%s%s/%s%s" % (self.cache_dir, str(pkey), str(media['id']), str(fileExt))
#logger.debug('No cue in/out detected for this file')
dst = "%s%s/%s%s" % (self.cache_dir, str(pkey), str(media['id']), str(fileExt))
do_cue = False
else:
#logger.debug('Cue in/out detected')
dst = "%s%s/%s_cue_%s-%s%s" % \
(self.cache_dir, str(pkey), str(media['id']), str(float(media['cue_in']) / 1000), str(float(media['cue_out']) / 1000), str(fileExt))
do_cue = True
# download media file # download media file
self.handle_remote_file(media, dst, do_cue) self.handle_remote_file(media, dst)
if True == os.access(dst, os.R_OK): if True == os.access(dst, os.R_OK):
# check filesize (avoid zero-byte files) # check filesize (avoid zero-byte files)
@ -230,11 +282,13 @@ class PypoFetch(Thread):
if fsize > 0: if fsize > 0:
pl_entry = \ pl_entry = \
'annotate:export_source="%s",media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",schedule_table_id="%s":%s'\ 'annotate:export_source="%s",media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \
% (str(media['export_source']), media['id'], 0, str(float(media['fade_in']) / 1000), \ % (str(media['export_source']), media['id'], 0, \
str(float(media['fade_out']) / 1000), media['row_id'],dst) str(float(media['fade_in']) / 1000), \
str(float(media['fade_out']) / 1000), \
#logger.debug(pl_entry) str(float(media['cue_in'])), \
str(float(media['cue_out'])), \
media['row_id'], dst)
""" """
Tracks are only added to the playlist if they are accessible Tracks are only added to the playlist if they are accessible
@ -248,7 +302,6 @@ class PypoFetch(Thread):
entry['show_name'] = playlist['show_name'] entry['show_name'] = playlist['show_name']
ls_playlist.append(entry) ls_playlist.append(entry)
#logger.debug("everything ok, adding %s to playlist", pl_entry)
else: else:
logger.warning("zero-size file - skipping %s. will not add it to playlist at %s", media['uri'], dst) logger.warning("zero-size file - skipping %s. will not add it to playlist at %s", media['uri'], dst)
@ -262,51 +315,14 @@ class PypoFetch(Thread):
""" """
Download a file from a remote server and store it in the cache. Download a file from a remote server and store it in the cache.
""" """
def handle_remote_file(self, media, dst, do_cue): def handle_remote_file(self, media, dst):
logger = logging.getLogger('fetch') logger = logging.getLogger('fetch')
if do_cue == False: if os.path.isfile(dst):
if os.path.isfile(dst): pass
pass #logger.debug("file already in cache: %s", dst)
#logger.debug("file already in cache: %s", dst)
else:
logger.debug("try to download %s", media['uri'])
self.api_client.get_media(media['uri'], dst)
else: else:
if os.path.isfile(dst): logger.debug("try to download %s", media['uri'])
logger.debug("file already in cache: %s", dst) self.api_client.get_media(media['uri'], dst)
else:
logger.debug("try to download and cue %s", media['uri'])
fileExt = os.path.splitext(media['uri'])[1]
dst_tmp = config["tmp_dir"] + "".join([random.choice(string.letters) for i in xrange(10)]) + fileExt
self.api_client.get_media(media['uri'], dst_tmp)
# cue
logger.debug("STARTING CUE")
debugDst = self.cue_file.cue(dst_tmp, dst, float(media['cue_in']) / 1000, float(media['cue_out']) / 1000)
logger.debug(debugDst)
logger.debug("END CUE")
if True == os.access(dst, os.R_OK):
try: fsize = os.path.getsize(dst)
except Exception, e:
logger.error("%s", e)
fsize = 0
if fsize > 0:
logger.debug('try to remove temporary file: %s' + dst_tmp)
try: os.remove(dst_tmp)
except Exception, e:
logger.error("%s", e)
else:
logger.warning('something went wrong cueing: %s - using uncued file' + dst)
try: os.rename(dst_tmp, dst)
except Exception, e:
logger.error("%s", e)
""" """
Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR" Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR"
@ -354,7 +370,7 @@ class PypoFetch(Thread):
# most recent schedule. After that we can just wait for updates. # most recent schedule. After that we can just wait for updates.
status, schedule_data = self.api_client.get_schedule() status, schedule_data = self.api_client.get_schedule()
if status == 1: if status == 1:
self.process_schedule(schedule_data, "scheduler") self.process_schedule(schedule_data, "scheduler", True)
logger.info("Bootstrap complete: got initial copy of the schedule") logger.info("Bootstrap complete: got initial copy of the schedule")
loops = 1 loops = 1
@ -373,6 +389,6 @@ class PypoFetch(Thread):
status, schedule_data = self.api_client.get_schedule() status, schedule_data = self.api_client.get_schedule()
if status == 1: if status == 1:
self.process_schedule(schedule_data, "scheduler") self.process_schedule(schedule_data, "scheduler", False)
loops += 1 loops += 1

View File

@ -97,9 +97,11 @@ class PypoPush(Thread):
str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5]) str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5])
for pkey in schedule: for pkey in schedule:
plstart = pkey[0:19] plstart = schedule[pkey]['start'][0:19]
#plstart = pkey[0:19]
playedFlag = (pkey in playedItems) and playedItems[pkey].get("played", 0) #playedFlag = (pkey in playedItems) and playedItems[pkey].get("played", 0)
playedFlag = False
if plstart == str_tcoming_s or (plstart < str_tcoming_s and plstart > str_tcoming2_s and not playedFlag): if plstart == str_tcoming_s or (plstart < str_tcoming_s and plstart > str_tcoming2_s and not playedFlag):
logger.debug('Preparing to push playlist scheduled at: %s', pkey) logger.debug('Preparing to push playlist scheduled at: %s', pkey)
@ -119,7 +121,6 @@ class PypoPush(Thread):
schedule_tracker = open(self.schedule_tracker_file, "w") schedule_tracker = open(self.schedule_tracker_file, "w")
pickle.dump(playedItems, schedule_tracker) pickle.dump(playedItems, schedule_tracker)
schedule_tracker.close() schedule_tracker.close()
#logger.debug("Wrote schedule to disk: "+str(json.dumps(playedItems)))
# Call API to update schedule states # Call API to update schedule states
logger.debug("Doing callback to server to update 'played' status.") logger.debug("Doing callback to server to update 'played' status.")
@ -132,7 +133,6 @@ class PypoPush(Thread):
currently_on_air = True currently_on_air = True
else: else:
pass pass
#logger.debug('Empty schedule')
if not currently_on_air and self.liquidsoap_state_play: if not currently_on_air and self.liquidsoap_state_play:
logger.debug('Notifying Liquidsoap to stop playback.') logger.debug('Notifying Liquidsoap to stop playback.')
@ -190,7 +190,6 @@ class PypoPush(Thread):
logger.debug('Preparing to push playlist %s' % pkey) logger.debug('Preparing to push playlist %s' % pkey)
for item in playlist: for item in playlist:
annotate = str(item['annotate']) annotate = str(item['annotate'])
#logger.debug(annotate)
tn.write(('queue.push %s\n' % annotate).encode('latin-1')) tn.write(('queue.push %s\n' % annotate).encode('latin-1'))
tn.write(('vars.show_name %s\n' % item['show_name']).encode('latin-1')) tn.write(('vars.show_name %s\n' % item['show_name']).encode('latin-1'))
@ -207,7 +206,6 @@ class PypoPush(Thread):
def load_schedule_tracker(self): def load_schedule_tracker(self):
logger = logging.getLogger('push') logger = logging.getLogger('push')
#logger.debug('load_schedule_tracker')
playedItems = dict() playedItems = dict()
# create the file if it doesnt exist # create the file if it doesnt exist
@ -220,7 +218,6 @@ class PypoPush(Thread):
except Exception, e: except Exception, e:
logger.error('Error creating schedule tracker file: %s', e) logger.error('Error creating schedule tracker file: %s', e)
else: else:
#logger.debug('schedule tracker file exists, opening: ' + self.schedule_tracker_file)
try: try:
schedule_tracker = open(self.schedule_tracker_file, "r") schedule_tracker = open(self.schedule_tracker_file, "r")
playedItems = pickle.load(schedule_tracker) playedItems = pickle.load(schedule_tracker)
@ -241,6 +238,6 @@ class PypoPush(Thread):
loops = 0 loops = 0
try: self.push('scheduler') try: self.push('scheduler')
except Exception, e: except Exception, e:
logger.error('Pypo Push Error, exiting: %s', e) logger.error('Pypo Push Exception: %s', e)
time.sleep(PUSH_INTERVAL) time.sleep(PUSH_INTERVAL)
loops += 1 loops += 1

View File

@ -1,4 +1,5 @@
import os import os
import sys
import time import time
def remove_user(username): def remove_user(username):

View File

@ -11,17 +11,20 @@
USERID=pypo USERID=pypo
GROUPID=pypo GROUPID=pypo
NAME=Airtime NAME=Airtime\ Show\ Recorder
DAEMON=/usr/bin/airtime-show-recorder DAEMON=/usr/lib/airtime/show-recorder/airtime-show-recorder
PIDFILE=/var/run/airtime-show-recorder.pid PIDFILE=/var/run/airtime-show-recorder.pid
start () { start () {
monit monitor airtime-show-recorder >/dev/null 2>&1
start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE --startas $DAEMON start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE --startas $DAEMON
} }
stop () { stop () {
# Send TERM after 5 seconds, wait at most 30 seconds. # Send TERM after 5 seconds, wait at most 30 seconds.
monit unmonitor airtime-show-recorder >/dev/null 2>&1
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE
rm -f $PIDFILE rm -f $PIDFILE
} }

View File

@ -63,14 +63,10 @@ try:
os.system("chmod -R 755 "+config["bin_dir"]) os.system("chmod -R 755 "+config["bin_dir"])
os.system("chown -R pypo:pypo "+config["bin_dir"]) os.system("chown -R pypo:pypo "+config["bin_dir"])
print "Creating symbolic links"
os.system("rm -f /usr/bin/airtime-show-recorder")
os.system("ln -s "+config["bin_dir"]+"/airtime-show-recorder /usr/bin/")
print "Installing show-recorder daemon" print "Installing show-recorder daemon"
shutil.copy(config["bin_dir"]+"/airtime-show-recorder-init-d", "/etc/init.d/airtime-show-recorder") shutil.copy(config["bin_dir"]+"/airtime-show-recorder-init-d", "/etc/init.d/airtime-show-recorder")
p = Popen("update-rc.d airtime-show-recorder defaults", shell=True) p = Popen("update-rc.d airtime-show-recorder defaults >/dev/null 2>&1", shell=True)
sts = os.waitpid(p.pid, 0)[1] sts = os.waitpid(p.pid, 0)[1]
print "Waiting for processes to start..." print "Waiting for processes to start..."

View File

@ -12,7 +12,7 @@ if os.geteuid() != 0:
PATH_INI_FILE = '/etc/airtime/recorder.cfg' PATH_INI_FILE = '/etc/airtime/recorder.cfg'
def remove_path(path): def remove_path(path):
os.system("rm -rf " + path) os.system('rm -rf "%s"' % path)
def get_current_script_dir(): def get_current_script_dir():
current_script_dir = os.path.realpath(__file__) current_script_dir = os.path.realpath(__file__)
@ -29,7 +29,7 @@ try:
os.system("/etc/init.d/airtime-show-recorder stop") os.system("/etc/init.d/airtime-show-recorder stop")
os.system("rm -f /etc/init.d/airtime-show-recorder") os.system("rm -f /etc/init.d/airtime-show-recorder")
os.system("update-rc.d -f airtime-show-recorder remove") os.system("update-rc.d -f airtime-show-recorder remove >/dev/null 2>&1")
print "Removing log directories" print "Removing log directories"
remove_path(config["log_dir"]) remove_path(config["log_dir"])