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
=======
Version 1.9.0
-------------
Same as previous version.
Version 1.8.2
-------------
Welcome to James Moon!

View File

@ -54,7 +54,7 @@ class ApiController extends Zend_Controller_Action
* Allows remote client to download requested media file.
*
* @return void
* The given value increased by the increment amount.
*
*/
public function getMediaAction()
{
@ -65,7 +65,7 @@ class ApiController extends Zend_Controller_Action
$this->_helper->viewRenderer->setNoRender(true);
$api_key = $this->_getParam('api_key');
$downlaod = $this->_getParam('download');
$download = ("true" == $this->_getParam('download'));
if(!in_array($api_key, $CC_CONFIG["apiKey"]))
{
@ -87,7 +87,6 @@ class ApiController extends Zend_Controller_Action
exit;
}
// possibly use fileinfo module here in the future.
// http://www.php.net/manual/en/book.fileinfo.php
$ext = pathinfo($filename, PATHINFO_EXTENSION);
@ -96,7 +95,12 @@ class ApiController extends Zend_Controller_Action
else if ($ext == "mp3")
header("Content-Type: audio/mpeg");
if ($download){
header('Content-Disposition: attachment; filename="'.$media->getName().'"');
//path_info breaks up a file path into seperate pieces of informaiton.
//We just want the basename which is the file name with the path
//information stripped away. We are using Content-Disposition to specify
//to the browser what name the file should be saved as.
$path_parts = pathinfo($media->getPropelOrm()->getDbFilepath());
header('Content-Disposition: attachment; filename="'.$path_parts['basename'].'"');
}
header("Content-Length: " . filesize($filepath));
@ -408,7 +412,8 @@ class ApiController extends Zend_Controller_Action
public function reloadMetadataAction() {
global $CC_CONFIG;
$api_key = $this->_getParam('api_key');
$request = $this->getRequest();
$api_key = $request->getParam('api_key');
if (!in_array($api_key, $CC_CONFIG["apiKey"]))
{
header('HTTP/1.0 401 Unauthorized');
@ -416,8 +421,16 @@ class ApiController extends Zend_Controller_Action
exit;
}
$md = $this->_getParam('md');
$mode = $this->_getParam('mode');
$mode = $request->getParam('mode');
$params = $request->getParams();
$md = array();
//extract all file metadata params from the request.
foreach ($params as $key => $value) {
if (preg_match('/^MDATA_KEY/', $key)) {
$md[$key] = $value;
}
}
if ($mode == "create") {
$md5 = $md['MDATA_KEY_MD5'];

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/datatables/js/jquery.dataTables.js','text/javascript');
$this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.pluginAPI.js','text/javascript');
$this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.fnSetFilteringDelay.js','text/javascript');
$this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/library.js','text/javascript');
$this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/advancedsearch.js','text/javascript');
@ -166,7 +167,7 @@ class LibraryController extends Zend_Controller_Action
$data = $file->getMetadata();
RabbitMq::SendFileMetaData($data);
RabbitMq::SendMessageToMediaMonitor("md_update", $data);
$this->_helper->redirector('index');
}

View File

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

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->statusMsg = "";
$form = new Application_Form_Preferences();
if ($request->isPost()) {
if ($form->isValid($request->getPost())) {
$values = $form->getValues();
Application_Model_Preference::SetHeadTitle($values["preferences_general"]["stationName"], $this->view);
Application_Model_Preference::SetDefaultFade($values["preferences_general"]["stationDefaultFade"]);
Application_Model_Preference::SetHeadTitle($values["preferences_general"]["stationName"], $this->view);
Application_Model_Preference::SetDefaultFade($values["preferences_general"]["stationDefaultFade"]);
Application_Model_Preference::SetStreamLabelFormat($values["preferences_general"]["streamFormat"]);
Application_Model_Preference::SetAllow3rdPartyApi($values["preferences_general"]["thirdPartyApi"]);
Application_Model_Preference::SetWatchedDirectory($values["preferences_general"]["watchedFolder"]);
Application_Model_Preference::SetDoSoundCloudUpload($values["preferences_soundcloud"]["UseSoundCloud"]);
Application_Model_Preference::SetDoSoundCloudUpload($values["preferences_soundcloud"]["UseSoundCloud"]);
Application_Model_Preference::SetSoundCloudUser($values["preferences_soundcloud"]["SoundCloudUser"]);
Application_Model_Preference::SetSoundCloudPassword($values["preferences_soundcloud"]["SoundCloudPassword"]);
Application_Model_Preference::SetSoundCloudPassword($values["preferences_soundcloud"]["SoundCloudPassword"]);
Application_Model_Preference::SetSoundCloudTags($values["preferences_soundcloud"]["SoundCloudTags"]);
Application_Model_Preference::SetSoundCloudGenre($values["preferences_soundcloud"]["SoundCloudGenre"]);
Application_Model_Preference::SetSoundCloudTrackType($values["preferences_soundcloud"]["SoundCloudTrackType"]);
@ -54,11 +55,13 @@ class PreferenceController extends Zend_Controller_Action
Application_Model_Preference::SetStationDescription($values["preferences_support"]["Description"]);
Application_Model_Preference::SetStationLogo($imagePath);
$data = array();
$data["directory"] = $values["preferences_general"]["watchedFolder"];
RabbitMq::SendMessageToMediaMonitor("new_watch", $data);
$this->view->statusMsg = "<div class='success'>Preferences updated.</div>";
}
}
$this->view->supportFeedback = Application_Model_Preference::GetSupportFeedback();
$logo = Application_Model_Preference::GetStationLogo();
if($logo){

View File

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

View File

@ -127,5 +127,35 @@ class DateHelper
$explode = explode(" ", $p_timestamp);
return $explode[1];
}
/* Given a track length in the format HH:MM:SS.mm, we want to
* convert this to seconds. This is useful for Liquidsoap which
* likes input parameters give in seconds.
* For example, 00:06:31.444, should be converted to 391.444 seconds
* @param int $p_time
* The time interval in format HH:MM:SS.mm we wish to
* convert to seconds.
* @return int
* The input parameter converted to seconds.
*/
public static function calculateLengthInSeconds($p_time){
if (2 !== substr_count($p_time, ":")){
return FALSE;
}
if (1 === substr_count($p_time, ".")){
list($hhmmss, $ms) = explode(".", $p_time);
} else {
$hhmmss = $p_time;
$ms = 0;
}
list($hours, $minutes, $seconds) = explode(":", $hhmmss);
$totalSeconds = $hours*3600 + $minutes*60 + $seconds + $ms/1000;
return $totalSeconds;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,6 +73,19 @@
</ul>
<?php endif; ?>
</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>
</fieldset>

View File

@ -175,5 +175,5 @@ $(document).ready(function() {
"oLanguage": {
"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 ***"
python ${SCRIPTPATH}/../python_apps/create-pypo-user.py
php ${SCRIPTPATH}/airtime-install.php $@
php ${SCRIPTPATH}/include/airtime-install.php $@
echo -e "\n*** Pypo Installation ***"
python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-install.py
@ -26,6 +26,7 @@ echo -e "\n*** Media Monitor Installation ***"
python ${SCRIPTPATH}/../python_apps/media-monitor/install/media-monitor-install.py
sleep 4
echo -e "\n*** Verifying your system environment ***"
airtime-check-system
echo -e "\n******************************* Install Complete *******************************"

View File

@ -1,5 +1,9 @@
#!/bin/bash
#Cause bash script to exit if any of the installers
#return with a non-zero return value.
set -e
# Absolute path to this script
SCRIPT=`readlink -f $0`
# Absolute directory this script is in
@ -7,8 +11,6 @@ SCRIPTPATH=`dirname $SCRIPT`
echo -e "\n******************************* Uninstall Begin ********************************"
php ${SCRIPTPATH}/airtime-uninstall.php
echo -e "\n*** Uninstalling Pypo ***"
python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-uninstall.py
@ -21,6 +23,9 @@ python ${SCRIPTPATH}/../python_apps/media-monitor/install/media-monitor-uninstal
echo -e "\n*** Removing Pypo User ***"
python ${SCRIPTPATH}/../python_apps/remove-pypo-user.py
php ${SCRIPTPATH}/include/airtime-uninstall.php
echo -e "\n****************************** Uninstall Complete ******************************\n"
echo "NOTE: To fully remove all Airtime files, you will also have to manually delete"
echo " the directories '/srv/airtime'(default storage location of media files)"

View File

@ -27,6 +27,7 @@ class AirtimeIni
const CONF_FILE_RECORDER = "/etc/airtime/recorder.cfg";
const CONF_FILE_LIQUIDSOAP = "/etc/airtime/liquidsoap.cfg";
const CONF_FILE_MEDIAMONITOR = "/etc/airtime/media-monitor.cfg";
const CONF_FILE_MONIT = "/etc/monit/conf.d/airtime-monit.cfg";
public static function IniFilesExist()
{
@ -75,10 +76,21 @@ class AirtimeIni
exit(1);
}
if (!copy(__DIR__."/../../python_apps/media-monitor/media-monitor.cfg", AirtimeIni::CONF_FILE_MEDIAMONITOR)){
echo "Could not copy MediaMonitor.cfg to /etc/airtime/. Exiting.";
echo "Could not copy media-monitor.cfg to /etc/airtime/. Exiting.";
exit(1);
}
}
public static function CreateMonitFile(){
if (!copy(__DIR__."/../../python_apps/monit/airtime-monit.cfg", AirtimeIni::CONF_FILE_MONIT)){
echo "Could not copy airtime-monit.cfg to /etc/monit/conf.d/. Exiting.";
exit(1);
}
}
public static function RemoveMonitFile(){
@unlink("/etc/monit/conf.d/airtime-monit.cfg");
}
/**
* This function removes /etc/airtime and the configuration
@ -187,7 +199,6 @@ class AirtimeIni
AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_PYPO, 'api_key', "'$api_key'");
AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_RECORDER, 'api_key', "'$api_key'");
AirtimeIni::UpdateIniValue(AirtimeIni::CONF_FILE_MEDIAMONITOR, 'api_key', "'$api_key'");
AirtimeIni::UpdateIniValue(AirtimeInstall::CONF_DIR_WWW.'/build/build.properties', 'project.home', AirtimeInstall::CONF_DIR_WWW);
}
public static function ReadPythonConfig($p_filename)

View File

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

View File

@ -8,10 +8,10 @@
* Performs a new install (new configs, database install) if a version of Airtime is not found
* If the current version is found to be installed the User is presented with the help menu and can choose -r to reinstall.
*/
set_include_path(__DIR__.'/../airtime_mvc/library' . PATH_SEPARATOR . get_include_path());
set_include_path(__DIR__.'/../../airtime_mvc/library' . PATH_SEPARATOR . get_include_path());
require_once(dirname(__FILE__).'/include/AirtimeIni.php');
require_once(dirname(__FILE__).'/include/AirtimeInstall.php');
require_once(dirname(__FILE__).'/AirtimeIni.php');
require_once(dirname(__FILE__).'/AirtimeInstall.php');
require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/constants.php');
AirtimeInstall::ExitIfNotRoot();
@ -97,6 +97,8 @@ if ($overwrite) {
echo "* Creating INI files".PHP_EOL;
AirtimeIni::CreateIniFiles();
}
AirtimeIni::CreateMonitFile();
AirtimeInstall::InstallPhpCode();
AirtimeInstall::InstallBinaries();
@ -106,6 +108,9 @@ if ($overwrite) {
AirtimeIni::UpdateIniFiles();
}
// Update the build.properties file to point to the correct directory.
AirtimeIni::UpdateIniValue(AirtimeInstall::CONF_DIR_WWW.'/build/build.properties', 'project.home', AirtimeInstall::CONF_DIR_WWW);
require_once(AirtimeInstall::GetAirtimeSrcDir().'/application/configs/conf.php');
echo "* Airtime Version: ".AIRTIME_VERSION.PHP_EOL;

View File

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

View File

@ -7,8 +7,9 @@
*/
//Pear classes.
set_include_path(__DIR__.'/../airtime_mvc/library/pear' . PATH_SEPARATOR . get_include_path());
set_include_path(__DIR__.'/../../airtime_mvc/library/pear' . PATH_SEPARATOR . get_include_path());
require_once('DB.php');
require_once(dirname(__FILE__).'/AirtimeIni.php');
if(exec("whoami") != "root"){
echo "Must be root user.\n";
@ -67,19 +68,19 @@ echo "******************************** Update Begin ****************************
$version = substr($version, 0, 5);
if (strcmp($version, "1.7.0") < 0){
system("php ".__DIR__."/upgrades/airtime-1.7/airtime-upgrade.php");
system("php ".__DIR__."/../upgrades/airtime-1.7/airtime-upgrade.php");
}
if (strcmp($version, "1.8.0") < 0){
system("php ".__DIR__."/upgrades/airtime-1.8/airtime-upgrade.php");
system("php ".__DIR__."/../upgrades/airtime-1.8/airtime-upgrade.php");
}
if (strcmp($version, "1.8.1") < 0){
system("php ".__DIR__."/upgrades/airtime-1.8.1/airtime-upgrade.php");
system("php ".__DIR__."/../upgrades/airtime-1.8.1/airtime-upgrade.php");
}
if (strcmp($version, "1.8.2") < 0){
system("php ".__DIR__."/upgrades/airtime-1.8.2/airtime-upgrade.php");
system("php ".__DIR__."/../upgrades/airtime-1.8.2/airtime-upgrade.php");
}
if (strcmp($version, "1.9.0") < 0){
system("php ".__DIR__."/upgrades/airtime-1.9/airtime-upgrade.php");
system("php ".__DIR__."/../upgrades/airtime-1.9/airtime-upgrade.php");
}
@ -91,13 +92,15 @@ $CC_DBC->query($sql);
echo PHP_EOL."*** Updating Pypo ***".PHP_EOL;
passthru("python ".__DIR__."/../python_apps/pypo/install/pypo-install.py");
passthru("python ".__DIR__."/../../python_apps/pypo/install/pypo-install.py");
echo PHP_EOL."*** Updating Recorder ***".PHP_EOL;
passthru("python ".__DIR__."/../python_apps/show-recorder/install/recorder-install.py");
passthru("python ".__DIR__."/../../python_apps/show-recorder/install/recorder-install.py");
echo PHP_EOL."*** Starting Media Monitor ***".PHP_EOL;
passthru("python ".__DIR__."/../python_apps/media-monitor/install/media-monitor-install.py");
echo PHP_EOL."*** Updating Media Monitor ***".PHP_EOL;
passthru("python ".__DIR__."/../../python_apps/media-monitor/install/media-monitor-install.py");
AirtimeIni::CreateMonitFile();
echo "******************************* Update Complete *******************************".PHP_EOL;

View File

@ -39,7 +39,7 @@ function InstallBinaries()
function UninstallBinaries()
{
echo "* Removing Airtime binaries from ".CONF_DIR_BINARIES.PHP_EOL;
exec("rm -rf ".CONF_DIR_BINARIES);
exec('rm -rf "'.CONF_DIR_BINARIES.'"');
}
@ -74,7 +74,7 @@ $pathnames = array("/usr/bin/airtime-pypo-start",
foreach ($pathnames as $pn){
echo "Removing $pn\n";
exec("rm -rf ".$pn);
exec("rm -rf \"$pn\"");
}

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')
sys.exit()
def recursive_urlencode(d):
def recursion(d, base=None):
pairs = []
for key, value in d.items():
if hasattr(value, 'values'):
pairs += recursion(value, key)
else:
new_pair = None
if base:
new_pair = "%s[%s]=%s" % (base, urllib.quote(unicode(key)), urllib.quote(unicode(value)))
else:
new_pair = "%s=%s" % (urllib.quote(unicode(key)), urllib.quote(unicode(value)))
pairs.append(new_pair)
return pairs
return '&'.join(recursion(d))
class ApiClientInterface:
# Implementation: optional
@ -402,11 +384,12 @@ class AirTimeApiClient(ApiClientInterface):
response = None
try:
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_media_url"])
logger.debug(url)
url = url.replace("%%api_key%%", self.config["api_key"])
url = url.replace("%%mode%%", mode)
logger.debug(url)
data = recursive_urlencode(md)
data = urllib.urlencode(md)
req = urllib2.Request(url, data)
response = urllib2.urlopen(req).read()
@ -636,7 +619,7 @@ class ObpApiClient():
def get_liquidsoap_data(self, pkey, schedule):
playlist = schedule[pkey]
data = dict()
data["ptype"] = playlist['subtype']
#data["ptype"] = playlist['subtype']
try:
data["user_id"] = playlist['user_id']
data["playlist_id"] = playlist['id']

View File

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

View File

@ -10,9 +10,12 @@ import hashlib
import json
import shutil
import math
import socket
import grp
import pwd
from collections import deque
from pwd import getpwnam
from subprocess import Popen, PIPE, STDOUT
from configobj import ConfigObj
@ -26,6 +29,8 @@ from kombu.connection import BrokerConnection
from kombu.messaging import Exchange, Queue, Consumer, Producer
from api_clients import api_client
from multiprocessing import Process, Lock
MODE_CREATE = "create"
MODE_MODIFY = "modify"
MODE_MOVED = "moved"
@ -54,10 +59,9 @@ list of supported easy tags in mutagen version 1.20
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', 'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', 'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', 'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', 'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', 'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', 'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', 'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak', 'organization']
"""
class AirtimeNotifier(Notifier):
class MetadataExtractor:
def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=0, timeout=None):
Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout)
def __init__(self):
self.airtime2mutagen = {\
"MDATA_KEY_TITLE": "title",\
@ -77,50 +81,6 @@ class AirtimeNotifier(Notifier):
"MDATA_KEY_COPYRIGHT": "copyright",\
}
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem")
self.connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/")
channel = self.connection.channel()
consumer = Consumer(channel, schedule_queue)
consumer.register_callback(self.handle_message)
consumer.consume()
self.logger = logging.getLogger('root')
def handle_message(self, body, message):
# ACK the message to take it off the queue
message.ack()
self.logger.info("Received md from RabbitMQ: " + body)
try:
m = json.loads(message.body)
airtime_file = mutagen.File(m['MDATA_KEY_FILEPATH'], easy=True)
for key in m.keys() :
if key in self.airtime2mutagen:
value = m[key]
if ((value is not None) and (len(str(value)) > 0)):
airtime_file[self.airtime2mutagen[key]] = str(value)
self.logger.info('setting %s = %s ', key, str(value))
airtime_file.save()
except Exception, e:
self.logger.error('Trying to save md')
self.logger.error('Exception: %s', e)
self.logger.error('Filepath %s', m['MDATA_KEY_FILEPATH'])
class MediaMonitor(ProcessEvent):
def my_init(self):
"""
Method automatically called from ProcessEvent.__init__(). Additional
keyworded arguments passed to ProcessEvent.__init__() are then
delegated to my_init().
"""
self.api_client = api_client.api_client_factory(config)
self.mutagen2airtime = {\
"title": "MDATA_KEY_TITLE",\
"artist": "MDATA_KEY_CREATOR",\
@ -139,26 +99,7 @@ class MediaMonitor(ProcessEvent):
"copyright": "MDATA_KEY_COPYRIGHT",\
}
self.supported_file_formats = ['mp3', 'ogg']
self.logger = logging.getLogger('root')
self.temp_files = {}
self.moved_files = {}
self.file_events = deque()
self.mask = pyinotify.ALL_EVENTS
self.wm = WatchManager()
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem")
connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/")
channel = connection.channel()
def watch_directory(self, directory):
return self.wm.add_watch(directory, self.mask, rec=True, auto_add=True)
def is_parent_directory(self, filepath, directory):
return (directory == filepath[0:len(directory)])
def get_md5(self, filepath):
f = open(filepath, 'rb')
@ -185,6 +126,192 @@ class MediaMonitor(ProcessEvent):
return length
def save_md_to_file(self, m):
try:
airtime_file = mutagen.File(m['MDATA_KEY_FILEPATH'], easy=True)
for key in m.keys() :
if key in self.airtime2mutagen:
value = m[key]
if ((value is not None) and (len(str(value)) > 0)):
airtime_file[self.airtime2mutagen[key]] = str(value)
#self.logger.info('setting %s = %s ', key, str(value))
airtime_file.save()
except Exception, e:
self.logger.error('Trying to save md')
self.logger.error('Exception: %s', e)
self.logger.error('Filepath %s', m['MDATA_KEY_FILEPATH'])
def get_md_from_file(self, filepath):
md = {}
md5 = self.get_md5(filepath)
md['MDATA_KEY_MD5'] = md5
file_info = mutagen.File(filepath, easy=True)
attrs = self.mutagen2airtime
for key in file_info.keys() :
if key in attrs :
md[attrs[key]] = file_info[key][0]
if 'MDATA_KEY_TITLE' not in md:
#get rid of file extention from original name, name might have more than 1 '.' in it.
original_name = os.path.basename(filepath)
original_name = original_name.split(".")[0:-1]
original_name = ''.join(original_name)
md['MDATA_KEY_TITLE'] = original_name
#incase track number is in format u'4/11'
if 'MDATA_KEY_TRACKNUMBER' in md:
if isinstance(md['MDATA_KEY_TRACKNUMBER'], basestring):
md['MDATA_KEY_TRACKNUMBER'] = md['MDATA_KEY_TRACKNUMBER'].split("/")[0]
md['MDATA_KEY_BITRATE'] = file_info.info.bitrate
md['MDATA_KEY_SAMPLERATE'] = file_info.info.sample_rate
md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length)
md['MDATA_KEY_MIME'] = file_info.mime[0]
if "mp3" in md['MDATA_KEY_MIME']:
md['MDATA_KEY_FTYPE'] = "audioclip"
elif "vorbis" in md['MDATA_KEY_MIME']:
md['MDATA_KEY_FTYPE'] = "audioclip"
#do this so object can be urlencoded properly.
for key in md.keys():
if(isinstance(md[key], basestring)):
md[key] = md[key].encode('utf-8')
return md
class AirtimeNotifier(Notifier):
def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=0, timeout=None):
Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout)
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem")
self.connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/")
channel = self.connection.channel()
consumer = Consumer(channel, schedule_queue)
consumer.register_callback(self.handle_message)
consumer.consume()
self.logger = logging.getLogger('root')
self.api_client = api_client.api_client_factory(config)
self.md_manager = MetadataExtractor()
self.import_processes = {}
self.watched_folders = []
def handle_message(self, body, message):
# ACK the message to take it off the queue
message.ack()
self.logger.info("Received md from RabbitMQ: " + body)
m = json.loads(message.body)
if m['event_type'] == "md_update":
self.logger.info("AIRTIME NOTIFIER md update event")
self.md_manager.save_md_to_file(m)
elif m['event_type'] == "new_watch":
self.logger.info("AIRTIME NOTIFIER add watched folder event " + m['directory'])
#start a new process to walk through this folder and add the files to Airtime.
p = Process(target=self.walk_newly_watched_directory, args=(m['directory'],))
p.start()
self.import_processes[m['directory']] = p
#add this new folder to our list of watched folders
self.watched_folders.append(m['directory'])
def update_airtime(self, d):
filepath = d['filepath']
mode = d['mode']
data = None
md = {}
md['MDATA_KEY_FILEPATH'] = filepath
if (os.path.exists(filepath) and (mode == MODE_CREATE)):
mutagen = self.md_manager.get_md_from_file(filepath)
md.update(mutagen)
data = md
elif (os.path.exists(filepath) and (mode == MODE_MODIFY)):
mutagen = self.md_manager.get_md_from_file(filepath)
md.update(mutagen)
data = md
elif (mode == MODE_MOVED):
mutagen = self.md_manager.get_md_from_file(filepath)
md.update(mutagen)
data = md
elif (mode == MODE_DELETE):
data = md
if data is not None:
self.logger.info("Updating Change to Airtime " + filepath)
response = None
while response is None:
response = self.api_client.update_media_metadata(data, mode)
time.sleep(5)
def walk_newly_watched_directory(self, directory):
for (path, dirs, files) in os.walk(directory):
for filename in files:
full_filepath = path+"/"+filename
self.update_airtime({'filepath': full_filepath, 'mode': MODE_CREATE})
class MediaMonitor(ProcessEvent):
def my_init(self):
"""
Method automatically called from ProcessEvent.__init__(). Additional
keyworded arguments passed to ProcessEvent.__init__() are then
delegated to my_init().
"""
self.api_client = api_client.api_client_factory(config)
self.supported_file_formats = ['mp3', 'ogg']
self.logger = logging.getLogger('root')
self.temp_files = {}
self.moved_files = {}
self.file_events = deque()
self.mask = pyinotify.ALL_EVENTS
self.wm = WatchManager()
self.md_manager = MetadataExtractor()
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem")
connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/")
channel = connection.channel()
def watch_directory(self, directory):
return self.wm.add_watch(directory, self.mask, rec=True, auto_add=True)
def is_parent_directory(self, filepath, directory):
return (directory == filepath[0:len(directory)])
def set_needed_file_permissions(self, item, is_dir):
try:
omask = os.umask(0)
uid = pwd.getpwnam('pypo')[2]
gid = grp.getgrnam('www-data')[2]
os.chown(item, uid, gid)
if is_dir is True:
os.chmod(item, 02777)
else:
os.chmod(item, 0666)
except Exception, e:
self.logger.error("Failed to change file's owner/group/permissions.")
self.logger.error(item)
finally:
os.umask(omask)
def ensure_dir(self, filepath):
directory = os.path.dirname(filepath)
@ -196,21 +323,38 @@ class MediaMonitor(ProcessEvent):
finally:
os.umask(omask)
def move_file(self, source, dest):
try:
omask = os.umask(0)
os.rename(source, dest)
except Exception, e:
self.logger.error("failed to move file.")
finally:
os.umask(omask)
def create_unique_filename(self, filepath):
if(os.path.exists(filepath)):
file_dir = os.path.dirname(filepath)
filename = os.path.basename(filepath).split(".")[0]
#will be in the format .ext
file_ext = os.path.splitext(filepath)[1]
i = 1;
while(True):
new_filepath = "%s/%s(%s)%s" % (file_dir, filename, i, file_ext)
try:
if(os.path.exists(filepath)):
self.logger.info("Path %s exists", filepath)
file_dir = os.path.dirname(filepath)
filename = os.path.basename(filepath).split(".")[0]
#will be in the format .ext
file_ext = os.path.splitext(filepath)[1]
i = 1;
while(True):
new_filepath = '%s/%s(%s)%s' % (file_dir, filename, i, file_ext)
self.logger.error("Trying %s", new_filepath)
if(os.path.exists(new_filepath)):
i = i+1;
else:
filepath = new_filepath
if(os.path.exists(new_filepath)):
i = i+1;
else:
filepath = new_filepath
break
except Exception, e:
self.logger.error("Exception %s", e)
return filepath
@ -226,23 +370,39 @@ class MediaMonitor(ProcessEvent):
#will be in the format .ext
file_ext = os.path.splitext(imported_filepath)[1]
md = self.get_mutagen_info(imported_filepath)
file_ext = file_ext.encode('utf-8')
md = self.md_manager.get_md_from_file(imported_filepath)
path_md = ['MDATA_KEY_TITLE', 'MDATA_KEY_CREATOR', 'MDATA_KEY_SOURCE', 'MDATA_KEY_TRACKNUMBER', 'MDATA_KEY_BITRATE']
self.logger.info('Getting md')
for m in path_md:
if m not in md:
md[m] = 'unknown'
md[m] = u'unknown'.encode('utf-8')
else:
#get rid of any "/" which will interfere with the filepath.
if isinstance(md[m], basestring):
md[m] = md[m].replace("/", "-")
self.logger.info(md)
self.logger.info('Starting filepath creation')
filepath = None
if (md['MDATA_KEY_TITLE'] == 'unknown'):
filepath = "%s/%s/%s/%s-%s%s" % (storage_directory, md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], original_name, md['MDATA_KEY_BITRATE'], file_ext)
elif(md['MDATA_KEY_TRACKNUMBER'] == 'unknown'):
filepath = "%s/%s/%s/%s-%s%s" % (storage_directory, md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext)
if (md['MDATA_KEY_TITLE'] == u'unknown'.encode('utf-8')):
self.logger.info('unknown title')
filepath = '%s/%s/%s/%s-%s%s' % (storage_directory.encode('utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], original_name, md['MDATA_KEY_BITRATE'], file_ext)
elif(md['MDATA_KEY_TRACKNUMBER'] == u'unknown'.encode('utf-8')):
self.logger.info('unknown track number')
filepath = '%s/%s/%s/%s-%s%s' % (storage_directory.encode('utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext)
else:
filepath = "%s/%s/%s/%s-%s-%s%s" % (storage_directory, md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TRACKNUMBER'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext)
self.logger.info('full metadata')
filepath = '%s/%s/%s/%s-%s-%s%s' % (storage_directory.encode('utf-8'), md['MDATA_KEY_CREATOR'], md['MDATA_KEY_SOURCE'], md['MDATA_KEY_TRACKNUMBER'], md['MDATA_KEY_TITLE'], md['MDATA_KEY_BITRATE'], file_ext)
self.logger.info(u'Created filepath: %s', filepath)
filepath = self.create_unique_filename(filepath)
self.logger.info(u'Unique filepath: %s', filepath)
self.ensure_dir(filepath)
except Exception, e:
@ -250,63 +410,6 @@ class MediaMonitor(ProcessEvent):
return filepath
def get_mutagen_info(self, filepath):
md = {}
md5 = self.get_md5(filepath)
md['MDATA_KEY_MD5'] = md5
file_info = mutagen.File(filepath, easy=True)
attrs = self.mutagen2airtime
for key in file_info.keys() :
if key in attrs :
md[attrs[key]] = file_info[key][0]
#md['MDATA_KEY_TRACKNUMBER'] = "%02d" % (int(md['MDATA_KEY_TRACKNUMBER']))
md['MDATA_KEY_BITRATE'] = file_info.info.bitrate
md['MDATA_KEY_SAMPLERATE'] = file_info.info.sample_rate
md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length)
md['MDATA_KEY_MIME'] = file_info.mime[0]
if "mp3" in md['MDATA_KEY_MIME']:
md['MDATA_KEY_FTYPE'] = "audioclip"
elif "vorbis" in md['MDATA_KEY_MIME']:
md['MDATA_KEY_FTYPE'] = "audioclip"
return md
def update_airtime(self, d):
filepath = d['filepath']
mode = d['mode']
data = None
md = {}
md['MDATA_KEY_FILEPATH'] = filepath
if (os.path.exists(filepath) and (mode == MODE_CREATE)):
mutagen = self.get_mutagen_info(filepath)
md.update(mutagen)
data = {'md': md}
elif (os.path.exists(filepath) and (mode == MODE_MODIFY)):
mutagen = self.get_mutagen_info(filepath)
md.update(mutagen)
data = {'md': md}
elif (mode == MODE_MOVED):
mutagen = self.get_mutagen_info(filepath)
md.update(mutagen)
data = {'md': md}
elif (mode == MODE_DELETE):
data = {'md': md}
if data is not None:
self.logger.info("Updating Change to Airtime")
response = None
while response is None:
response = self.api_client.update_media_metadata(data, mode)
time.sleep(5)
def is_temp_file(self, filename):
info = filename.split(".")
@ -334,18 +437,20 @@ class MediaMonitor(ProcessEvent):
global plupload_directory
#files that have been added through plupload have a placeholder already put in Airtime's database.
if not self.is_parent_directory(event.pathname, plupload_directory):
md5 = self.get_md5(event.pathname)
response = self.api_client.check_media_status(md5)
if self.is_audio_file(event.pathname):
self.set_needed_file_permissions(event.pathname, event.dir)
md5 = self.md_manager.get_md5(event.pathname)
response = self.api_client.check_media_status(md5)
#this file is new, md5 does not exist in Airtime.
if(response['airtime_status'] == 0):
filepath = self.create_file_path(event.pathname)
os.rename(event.pathname, filepath)
self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath})
#this file is new, md5 does not exist in Airtime.
if(response['airtime_status'] == 0):
filepath = self.create_file_path(event.pathname)
self.move_file(event.pathname, filepath)
self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath})
#immediately add a watch on the new directory.
else:
self.watch_directory(event.pathname)
self.set_needed_file_permissions(event.pathname, event.dir)
def process_IN_MODIFY(self, event):
if not event.dir:
@ -367,6 +472,8 @@ class MediaMonitor(ProcessEvent):
def process_IN_MOVED_TO(self, event):
self.logger.info("%s: %s", event.maskname, event.pathname)
#if stuff dropped in stor via a UI move must change file permissions.
self.set_needed_file_permissions(event.pathname, event.dir)
if not event.dir:
if event.cookie in self.temp_files:
del self.temp_files[event.cookie]
@ -380,7 +487,7 @@ class MediaMonitor(ProcessEvent):
#file renamed from /tmp/plupload does not have a path in our naming scheme yet.
md_filepath = self.create_file_path(event.pathname)
#move the file a second time to its correct Airtime naming schema.
os.rename(event.pathname, md_filepath)
self.move_file(event.pathname, md_filepath)
self.file_events.append({'filepath': md_filepath, 'mode': MODE_MOVED})
else:
self.file_events.append({'filepath': event.pathname, 'mode': MODE_MOVED})
@ -389,7 +496,7 @@ class MediaMonitor(ProcessEvent):
#TODO need to pass in if md5 exists to this file creation function, identical files will just replace current files not have a (1) etc.
#file has been most likely dropped into stor folder from an unwatched location. (from gui, mv command not cp)
md_filepath = self.create_file_path(event.pathname)
os.rename(event.pathname, md_filepath)
self.move_file(event.pathname, md_filepath)
self.file_events.append({'mode': MODE_CREATE, 'filepath': md_filepath})
def process_IN_DELETE(self, event):
@ -402,12 +509,22 @@ class MediaMonitor(ProcessEvent):
def notifier_loop_callback(self, notifier):
for watched_directory in notifier.import_processes.keys():
process = notifier.import_processes[watched_directory]
if not process.is_alive():
self.watch_directory(watched_directory)
del notifier.import_processes[watched_directory]
while len(self.file_events) > 0:
self.logger.info("Processing a file event update to Airtime.")
file_info = self.file_events.popleft()
self.update_airtime(file_info)
notifier.update_airtime(file_info)
try:
notifier.connection.drain_events(timeout=1)
#avoid logging a bunch of timeout messages.
except socket.timeout:
pass
except Exception, e:
self.logger.info("%s", e)

View File

@ -9,19 +9,21 @@
# Short-Description: Manage airtime-media-monitor daemon
### END INIT INFO
USERID=pypo
GROUPID=pypo
NAME=Airtime
USERID=root
GROUPID=www-data
NAME=Airtime\ Media\ Monitor
DAEMON=/usr/bin/airtime-media-monitor
DAEMON=/usr/lib/airtime/media-monitor/airtime-media-monitor
PIDFILE=/var/run/airtime-media-monitor.pid
start () {
start () {
monit monitor airtime-media-monitor >/dev/null 2>&1
start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE --startas $DAEMON
}
stop () {
# Send TERM after 5 seconds, wait at most 30 seconds.
monit unmonitor airtime-media-monitor >/dev/null 2>&1
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE
rm -f $PIDFILE
}

View File

@ -34,7 +34,7 @@ def copy_dir(src_dir, dest_dir):
if not (os.path.exists(dest_dir)):
print "Copying directory "+os.path.realpath(src_dir)+" to "+os.path.realpath(dest_dir)
shutil.copytree(src_dir, dest_dir)
def get_current_script_dir():
current_script_dir = os.path.realpath(__file__)
index = current_script_dir.rindex('/')
@ -60,21 +60,18 @@ try:
os.system("chown -R pypo:pypo "+config["log_dir"])
copy_dir("%s/.."%current_script_dir, config["bin_dir"])
print "Setting permissions"
os.system("chmod -R 755 "+config["bin_dir"])
#os.system("chmod -R 755 "+config["bin_dir"]+"/airtime-media-monitor)
os.system("chown -R pypo:pypo "+config["bin_dir"])
print "Creating symbolic links"
os.system("rm -f /usr/bin/airtime-media-monitor")
os.system("ln -s "+config["bin_dir"]+"/airtime-media-monitor /usr/bin/")
print "Installing media-monitor daemon"
shutil.copy(config["bin_dir"]+"/airtime-media-monitor-init-d", "/etc/init.d/airtime-media-monitor")
p = Popen("update-rc.d airtime-media-monitor defaults", shell=True)
p = Popen("update-rc.d airtime-media-monitor defaults >/dev/null 2>&1", shell=True)
sts = os.waitpid(p.pid, 0)[1]
print "Waiting for processes to start..."
p = Popen("/etc/init.d/airtime-media-monitor start", shell=True)
sts = os.waitpid(p.pid, 0)[1]

View File

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

View File

@ -1,5 +1,10 @@
set daemon 10 # Poll at 10 second intervals
set logfile syslog facility log_daemon
set httpd port 2812 and use address 127.0.0.1
allow localhost
allow admin:monit
check process airtime-playout
with pidfile "/var/run/airtime-playout.pid"
start program = "/etc/init.d/airtime-playout start" with timeout 10 seconds

View File

@ -11,24 +11,31 @@
USERID=pypo
GROUPID=pypo
NAME=Airtime
NAME=Airtime\ Playout
DAEMON0=/usr/bin/airtime-playout
DAEMON0=/usr/lib/airtime/pypo/bin/airtime-playout
PIDFILE0=/var/run/airtime-playout.pid
DAEMON1=/usr/bin/airtime-liquidsoap
DAEMON1=/usr/lib/airtime/pypo/bin/airtime-liquidsoap
PIDFILE1=/var/run/airtime-liquidsoap.pid
start () {
start () {
monit monitor airtime-playout >/dev/null 2>&1
start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE0 --startas $DAEMON0
monit monitor airtime-liquidsoap >/dev/null 2>&1
start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID --make-pidfile --pidfile $PIDFILE1 --startas $DAEMON1
}
stop () {
# Send TERM after 5 seconds, wait at most 30 seconds.
monit unmonitor airtime-playout >/dev/null 2>&1
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE0
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE1
rm -f $PIDFILE0
monit unmonitor airtime-liquidsoap >/dev/null 2>&1
start-stop-daemon --stop --oknodo --retry TERM/5/0/30 --quiet --pidfile $PIDFILE1
rm -f $PIDFILE1
}

View File

@ -81,10 +81,10 @@ try:
if architecture == '64bit' and natty:
print "Installing 64-bit liquidsoap binary (Natty)"
shutil.copy("%s/../liquidsoap_bin/liquidsoap-amd64-natty"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir)
shutil.copy("%s/../liquidsoap_bin/liquidsoap-natty-amd64"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir)
elif architecture == '32bit' and natty:
print "Installing 32-bit liquidsoap binary (Natty)"
shutil.copy("%s/../liquidsoap_bin/liquidsoap-i386-natty"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir)
shutil.copy("%s/../liquidsoap_bin/liquidsoap-natty-i386"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir)
elif architecture == '64bit' and not natty:
print "Installing 64-bit liquidsoap binary"
shutil.copy("%s/../liquidsoap_bin/liquidsoap-amd64"%current_script_dir, "%s/../liquidsoap_bin/liquidsoap"%current_script_dir)
@ -103,16 +103,10 @@ try:
os.system("chown -R pypo:pypo "+config["bin_dir"])
os.system("chown -R pypo:pypo "+config["cache_base_dir"])
print "Creating symbolic links"
os.system("rm -f /usr/bin/airtime-playout")
os.system("ln -s "+config["bin_dir"]+"/bin/airtime-playout /usr/bin/")
os.system("rm -f /usr/bin/airtime-liquidsoap")
os.system("ln -s "+config["bin_dir"]+"/bin/airtime-liquidsoap /usr/bin/")
print "Installing pypo daemon"
shutil.copy(config["bin_dir"]+"/bin/airtime-playout-init-d", "/etc/init.d/airtime-playout")
p = Popen("update-rc.d airtime-playout defaults", shell=True)
p = Popen("update-rc.d airtime-playout defaults >/dev/null 2>&1", shell=True)
sts = os.waitpid(p.pid, 0)[1]
print "Waiting for processes to start..."

View File

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

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 \
$(wildcard *.liq) extract-replaygain

View File

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

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
end
# Register a command that outputs the RMS of the returned source.
# @category Source / Visualization
# @param ~id Force the value of the source ID.
def server.rms(~id="",s) =
x = rms(id=id,s)
rms = fst(x)
s = snd(x)
id = source.id(s)
def rms(_) =
rms = rms()
"#{rms}"
end
server.register(namespace="#{id}",
description="Return the current RMS of the source.",
usage="rms",
"rms",rms)
s
end
# Get the base name of a path.
# Implemented using the corresponding shell command.
# @category System
@ -479,59 +498,95 @@ def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
end
# Custom playlist source written using the script language.
# Will read directory or playlist, play all files and stop
# Will read directory or playlist, play all files and stop.
# Returns a pair @(reload,source)@ where @reload@ is a function
# of type @(?uri:string)->unit@ used to reload the source and @source@
# is the actual source. The reload function can optionally be called
# with a new playlist URI. Otherwise, it reloads the previous URI.
# @category Source / Input
# @param ~id Force the value of the source ID.
# @param ~random Randomize playlist content
# @param ~on_done Function to execute when the playlist is finished
# @param uri Playlist URI
def playlist.once(~random=false,~on_done={()},uri)
x = ref 0
def playlist.custom(files)
length = list.length(files)
if length == 0 then
log("Empty playlist..")
fail ()
else
files =
if random then
list.sort(fun (x,y) -> int_of_float(random.float()), files)
else
files
end
def next () =
state = !x
file =
if state < length then
x := state + 1
list.nth(files,state)
else
# Playlist finished
def playlist.reloadable(~id="",~random=false,~on_done={()},uri)
# A reference to the playlist
playlist = ref []
# A reference to the uri
playlist_uri = ref uri
# A reference to know if the source
# has been stopped
has_stopped = ref false
# The next function
def next () =
file =
if list.length(!playlist) > 0 then
ret = list.hd(!playlist)
playlist := list.tl(!playlist)
ret
else
# Playlist finished
if not !has_stopped then
on_done ()
""
end
request.create(file)
has_stopped := true
""
end
request.dynamic(next)
request.create(file)
end
# Instanciate the source
source = request.dynamic(id=id,next)
# Get its id.
id = source.id(source)
# The load function
def load_playlist () =
files =
if test_process("test -d #{quote(!playlist_uri)}") then
log(label=id,"playlist is a directory.")
get_process_lines("find #{quote(!playlist_uri)} -type f | sort")
else
playlist = request.create.raw(!playlist_uri)
result =
if request.resolve(playlist) then
playlist = request.filename(playlist)
files = playlist.parse(playlist)
list.map(snd,files)
else
log(label=id,"Couldn't read playlist: request resolution failed.")
[]
end
request.destroy(playlist)
result
end
if random then
playlist := list.sort(fun (x,y) -> int_of_float(random.float()), files)
else
playlist := files
end
end
if test_process("test -d #{quote(uri)}") then
files = get_process_lines("find #{quote(uri)} -type f | sort")
playlist.custom(files)
else
playlist = request.create.raw(uri)
result =
if request.resolve(playlist) then
playlist = request.filename(playlist)
files = playlist.parse(playlist)
files = list.map(snd,files)
playlist.custom(files)
else
log("Couldn't read playlist: request resolution failed.")
fail ()
end
request.destroy(playlist)
result
# The reload function
def reload(~uri="") =
if uri != "" then
playlist_uri := uri
end
log(label=id,"Reloading playlist with URI #{!playlist_uri}")
has_stopped := false
load_playlist()
end
# Load the playlist
load_playlist()
# Return
(reload,source)
end
# Custom playlist source written using the script language.
# Will read directory or playlist, play all files and stop
# @category Source / Input
# @param ~id Force the value of the source ID.
# @param ~random Randomize playlist content
# @param ~on_done Function to execute when the playlist is finished
# @param uri Playlist URI
def playlist.once(~id="",~random=false,~on_done={()},uri)
snd(playlist.reloadable(id=id,random=random,on_done=on_done,uri))
end
# Mixes two streams, with faded transitions between the state when only the
@ -588,7 +643,8 @@ def exec_at(~freq=1.,~pred,f)
add_timeout(freq,check)
end
# Register the replaygain protocol
# Register the replaygain protocol.
# @category Liquidsoap
def replaygain_protocol(arg,delay)
# The extraction program
extract_replaygain = "#{configure.libdir}/extract-replaygain"

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_level = 3
###########################################
# stream settings #
# Icecast Stream settings #
###########################################
icecast_host = "127.0.0.1"
icecast_port = 8000
icecast_pass = "hackme"
###########################################
# webstream mountpoint names #
###########################################
# Icecast mountpoint names
mount_point_mp3 = "airtime.mp3"
mount_point_vorbis = "airtime.ogg"
###########################################
# webstream metadata settings #
###########################################
# Webstream metadata settings
icecast_url = "http://airtime.sourcefabric.org"
icecast_description = "Airtime Radio!"
icecast_genre = "genre"
###########################################
#liquidsoap output settings #
###########################################
output_sound_device = false
output_icecast_vorbis = true
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!
# Audio stream metadata for vorbis/ogg is disabled by default
# due to a number of client media players that disconnect
# when the metadata changes to a new track. Some versions of
# mplayer and VLC have this problem. Enable this option at your
# own risk!
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)
queue = request.queue(id="queue", length=0.5)
queue = cue_cut(queue)
queue = audio_to_stereo(queue)
pypo_data = ref '0'
@ -57,8 +58,10 @@ end
if output_icecast_mp3 then
out_mp3 = output.icecast(%mp3,
host = icecast_host, port = icecast_port,
password = icecast_pass, mount = mount_point_mp3,
host = icecast_host,
port = icecast_port,
password = icecast_pass,
mount = mount_point_mp3,
fallible = true,
restart = true,
restart_delay = 5,
@ -69,11 +72,12 @@ if output_icecast_mp3 then
end
if output_icecast_vorbis then
if output_icecast_vorbis_metadata then
out_vorbis = output.icecast(%vorbis,
host = icecast_host, port = icecast_port,
password = icecast_pass, mount = mount_point_vorbis,
host = icecast_host,
port = icecast_port,
password = icecast_pass,
mount = mount_point_vorbis,
fallible = true,
restart = true,
restart_delay = 5,
@ -86,16 +90,30 @@ if output_icecast_vorbis then
#with vlc and mplayer disconnecting at the end of every track
s = add(normalize=false, [amplify(0.00001, noise()),s])
out_vorbis = output.icecast(%vorbis,
host = icecast_host, port = icecast_port,
password = icecast_pass, mount = mount_point_vorbis,
host = icecast_host,
port = icecast_port,
password = icecast_pass,
mount = mount_point_vorbis,
fallible = true,
restart = true,
restart_delay = 5,
url = icecast_url,
description = icecast_description,
genre = icecast_genre,
s)
s)
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 '\033[0;32m%s %s\033[m' % ('scheduled at:', str(pkey))
print 'cached at : ' + self.cache_dir + str(pkey)
print 'subtype: ' + str(playlist['subtype'])
print 'played: ' + str(playlist['played'])
print 'schedule id: ' + str(playlist['schedule_id'])
print 'duration: ' + str(playlist['duration'])

View File

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

View File

@ -4,7 +4,6 @@ import time
import logging
import logging.config
import shutil
import pickle
import random
import string
import json
@ -12,6 +11,8 @@ import telnetlib
import math
from threading import Thread
from subprocess import Popen, PIPE
from datetime import datetime
from datetime import timedelta
# For RabbitMQ
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(" * 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.")
"""
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
- Reads the scheduled entries of a given range (actual time +/- "prepare_ahead" / "cache_for")
- Saves a serialized file of the schedule
- 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)
- 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')
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"])
# Push stream metadata to liquidsoap
@ -127,9 +168,9 @@ class PypoFetch(Thread):
logger.error("Exception %s", e)
status = 0
# Download all the media and put playlists in liquidsoap format
# Download all the media and put playlists in liquidsoap "annotate" format
try:
liquidsoap_playlists = self.prepare_playlists(playlists)
liquidsoap_playlists = self.prepare_playlists(playlists, bootstrapping)
except Exception, e: logger.error("%s", e)
# Send the data to pypo-push
@ -149,7 +190,7 @@ class PypoFetch(Thread):
and stored in a playlist folder.
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')
liquidsoap_playlists = dict()
@ -170,27 +211,18 @@ class PypoFetch(Thread):
try:
os.mkdir(self.cache_dir + str(pkey))
except Exception, e:
pass
logger.error(e)
#logger.debug('*****************************************')
#logger.debug('pkey: ' + str(pkey))
#logger.debug('cached at : ' + self.cache_dir + str(pkey))
#logger.debug('subtype: ' + str(playlist['subtype']))
#logger.debug('played: ' + str(playlist['played']))
#logger.debug('schedule id: ' + str(playlist['schedule_id']))
#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)
#June 13, 2011: Commented this block out since we are not currently setting this to '1'
#on the server side. Currently using a different method to detect if already played - Martin
#if int(playlist['played']) == 1:
# logger.info("playlist %s already played / sent to liquidsoap, so will ignore it", pkey)
ls_playlist = self.handle_media_file(playlist, pkey, bootstrapping)
liquidsoap_playlists[pkey] = ls_playlist
except Exception, e:
logger.info("%s", e)
logger.error("%s", e)
return liquidsoap_playlists
@ -199,27 +231,47 @@ class PypoFetch(Thread):
This handles both remote and local files.
Returns an updated ls_playlist string.
"""
def handle_media_file(self, playlist, pkey):
ls_playlist = []
def handle_media_file(self, playlist, pkey, bootstrapping):
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'])
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]
try:
if str(media['cue_in']) == '0' and str(media['cue_out']) == '0':
#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
dst = "%s%s/%s%s" % (self.cache_dir, str(pkey), str(media['id']), str(fileExt))
# 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):
# check filesize (avoid zero-byte files)
@ -230,11 +282,13 @@ class PypoFetch(Thread):
if fsize > 0:
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'\
% (str(media['export_source']), media['id'], 0, str(float(media['fade_in']) / 1000), \
str(float(media['fade_out']) / 1000), media['row_id'],dst)
#logger.debug(pl_entry)
'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(float(media['fade_out']) / 1000), \
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
@ -248,7 +302,6 @@ class PypoFetch(Thread):
entry['show_name'] = playlist['show_name']
ls_playlist.append(entry)
#logger.debug("everything ok, adding %s to playlist", pl_entry)
else:
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.
"""
def handle_remote_file(self, media, dst, do_cue):
def handle_remote_file(self, media, dst):
logger = logging.getLogger('fetch')
if do_cue == False:
if os.path.isfile(dst):
pass
#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)
if os.path.isfile(dst):
pass
#logger.debug("file already in cache: %s", dst)
else:
if os.path.isfile(dst):
logger.debug("file already in cache: %s", 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)
logger.debug("try to download %s", media['uri'])
self.api_client.get_media(media['uri'], dst)
"""
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.
status, schedule_data = self.api_client.get_schedule()
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")
loops = 1
@ -373,6 +389,6 @@ class PypoFetch(Thread):
status, schedule_data = self.api_client.get_schedule()
if status == 1:
self.process_schedule(schedule_data, "scheduler")
self.process_schedule(schedule_data, "scheduler", False)
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])
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):
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")
pickle.dump(playedItems, schedule_tracker)
schedule_tracker.close()
#logger.debug("Wrote schedule to disk: "+str(json.dumps(playedItems)))
# Call API to update schedule states
logger.debug("Doing callback to server to update 'played' status.")
@ -132,7 +133,6 @@ class PypoPush(Thread):
currently_on_air = True
else:
pass
#logger.debug('Empty schedule')
if not currently_on_air and self.liquidsoap_state_play:
logger.debug('Notifying Liquidsoap to stop playback.')
@ -190,7 +190,6 @@ class PypoPush(Thread):
logger.debug('Preparing to push playlist %s' % pkey)
for item in playlist:
annotate = str(item['annotate'])
#logger.debug(annotate)
tn.write(('queue.push %s\n' % annotate).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):
logger = logging.getLogger('push')
#logger.debug('load_schedule_tracker')
playedItems = dict()
# create the file if it doesnt exist
@ -220,7 +218,6 @@ class PypoPush(Thread):
except Exception, e:
logger.error('Error creating schedule tracker file: %s', e)
else:
#logger.debug('schedule tracker file exists, opening: ' + self.schedule_tracker_file)
try:
schedule_tracker = open(self.schedule_tracker_file, "r")
playedItems = pickle.load(schedule_tracker)
@ -241,6 +238,6 @@ class PypoPush(Thread):
loops = 0
try: self.push('scheduler')
except Exception, e:
logger.error('Pypo Push Error, exiting: %s', e)
logger.error('Pypo Push Exception: %s', e)
time.sleep(PUSH_INTERVAL)
loops += 1

View File

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

View File

@ -11,17 +11,20 @@
USERID=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
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
}
stop () {
# 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
rm -f $PIDFILE
}

View File

@ -63,14 +63,10 @@ try:
os.system("chmod -R 755 "+config["bin_dir"])
os.system("chown -R pypo:pypo "+config["bin_dir"])
print "Creating symbolic links"
os.system("rm -f /usr/bin/airtime-show-recorder")
os.system("ln -s "+config["bin_dir"]+"/airtime-show-recorder /usr/bin/")
print "Installing show-recorder daemon"
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]
print "Waiting for processes to start..."

View File

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