");
}
- if (criteriaTypes[ele.mDataProp] == "s") {
+ if (libraryColumnTypes[ele.mDataProp] == "s") {
var obj = { sSelector: "#"+ele.mDataProp }
} else {
var obj = { sSelector: "#"+ele.mDataProp, type: "number-range" }
@@ -435,10 +460,15 @@ var AIRTIME = (function(AIRTIME) {
// put hidden columns at the top to insure they can never be visible
// on the table through column reordering.
+
+ //IMPORTANT: WHEN ADDING A NEW COLUMN PLEASE CONSULT WITH THE WIKI
+ // https://wiki.sourcefabric.org/display/CC/Adding+a+new+library+datatable+column
"aoColumns": [
/* ftype */ { "sTitle" : "" , "mDataProp" : "ftype" , "bSearchable" : false , "bVisible" : false } ,
/* Checkbox */ { "sTitle" : "" , "mDataProp" : "checkbox" , "bSortable" : false , "bSearchable" : false , "sWidth" : "25px" , "sClass" : "library_checkbox" } ,
/* Type */ { "sTitle" : "" , "mDataProp" : "image" , "bSearchable" : false , "sWidth" : "25px" , "sClass" : "library_type" , "iDataSort" : 0 } ,
+ /* Is Scheduled */ { "sTitle" : $.i18n._("Scheduled") , "mDataProp" : "is_scheduled" , "bSearchable" : false , "sWidth" : "90px" , "sClass" : "library_is_scheduled"} ,
+ /* Is Playlist */ { "sTitle" : $.i18n._("Playlist") , "mDataProp" : "is_playlist" , "bSearchable" : false , "sWidth" : "70px" , "sClass" : "library_is_playlist"} ,
/* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "track_title" , "sClass" : "library_title" , "sWidth" : "170px" } ,
/* Creator */ { "sTitle" : $.i18n._("Creator") , "mDataProp" : "artist_name" , "sClass" : "library_creator" , "sWidth" : "160px" } ,
/* Album */ { "sTitle" : $.i18n._("Album") , "mDataProp" : "album_title" , "sClass" : "library_album" , "sWidth" : "150px" } ,
@@ -447,6 +477,8 @@ var AIRTIME = (function(AIRTIME) {
/* Composer */ { "sTitle" : $.i18n._("Composer") , "mDataProp" : "composer" , "bVisible" : false , "sClass" : "library_composer" , "sWidth" : "150px" },
/* Conductor */ { "sTitle" : $.i18n._("Conductor") , "mDataProp" : "conductor" , "bVisible" : false , "sClass" : "library_conductor" , "sWidth" : "125px" },
/* Copyright */ { "sTitle" : $.i18n._("Copyright") , "mDataProp" : "copyright" , "bVisible" : false , "sClass" : "library_copyright" , "sWidth" : "125px" },
+ /* Cue In */ { "sTitle" : $.i18n._("Cue In") , "mDataProp" : "cuein" , "bVisible" : false , "sClass" : "library_length" , "sWidth" : "80px" },
+ /* Cue Out */ { "sTitle" : $.i18n._("Cue Out") , "mDataProp" : "cueout" , "bVisible" : false , "sClass" : "library_length" , "sWidth" : "80px" },
/* Encoded */ { "sTitle" : $.i18n._("Encoded By") , "mDataProp" : "encoded_by" , "bVisible" : false , "sClass" : "library_encoded" , "sWidth" : "150px" },
/* Genre */ { "sTitle" : $.i18n._("Genre") , "mDataProp" : "genre" , "bVisible" : false , "sClass" : "library_genre" , "sWidth" : "100px" },
/* ISRC Number */ { "sTitle" : $.i18n._("ISRC") , "mDataProp" : "isrc_number" , "bVisible" : false , "sClass" : "library_isrc" , "sWidth" : "150px" },
@@ -491,9 +523,11 @@ var AIRTIME = (function(AIRTIME) {
},
"fnStateLoad": function fnLibStateLoad(oSettings) {
var settings = localStorage.getItem('datatables-library');
-
- if (settings !== "") {
+
+ try {
return JSON.parse(settings);
+ } catch (e) {
+ return null;
}
},
"fnStateLoadParams": function (oSettings, oData) {
@@ -501,18 +535,22 @@ var AIRTIME = (function(AIRTIME) {
length,
a = oData.abVisCols;
- // putting serialized data back into the correct js type to make
- // sure everything works properly.
- for (i = 0, length = a.length; i < length; i++) {
- if (typeof(a[i]) === "string") {
- a[i] = (a[i] === "true") ? true : false;
- }
+ if (a) {
+ // putting serialized data back into the correct js type to make
+ // sure everything works properly.
+ for (i = 0, length = a.length; i < length; i++) {
+ if (typeof(a[i]) === "string") {
+ a[i] = (a[i] === "true") ? true : false;
+ }
+ }
}
-
+
a = oData.ColReorder;
- for (i = 0, length = a.length; i < length; i++) {
- if (typeof(a[i]) === "string") {
- a[i] = parseInt(a[i], 10);
+ if (a) {
+ for (i = 0, length = a.length; i < length; i++) {
+ if (typeof(a[i]) === "string") {
+ a[i] = parseInt(a[i], 10);
+ }
}
}
@@ -554,12 +592,46 @@ var AIRTIME = (function(AIRTIME) {
},
"fnRowCallback": AIRTIME.library.fnRowCallback,
"fnCreatedRow": function( nRow, aData, iDataIndex ) {
-
+ //add soundcloud icon
+ if (aData.soundcloud_status !== undefined) {
+ if (aData.soundcloud_status === "-2") {
+ $(nRow).find("td.library_title").append('');
+ } else if (aData.soundcloud_status === "-3") {
+ $(nRow).find("td.library_title").append('');
+ } else if (aData.soundcloud_status !== null) {
+ $(nRow).find("td.library_title").append('');
+ }
+ }
+
+ // add checkbox
+ $(nRow).find('td.library_checkbox').html("");
+
+ // add audio preview image/button
+ if (aData.ftype === "audioclip") {
+ $(nRow).find('td.library_type').html('');
+ } else if (aData.ftype === "playlist") {
+ $(nRow).find('td.library_type').html('');
+ } else if (aData.ftype === "block") {
+ $(nRow).find('td.library_type').html('');
+ } else if (aData.ftype === "stream") {
+ $(nRow).find('td.library_type').html('');
+ }
+
+ if (aData.is_scheduled) {
+ $(nRow).find("td.library_is_scheduled").html('');
+ } else if (!aData.is_scheduled) {
+ $(nRow).find("td.library_is_scheduled").html('');
+ }
+ if (aData.is_playlist) {
+ $(nRow).find("td.library_is_playlist").html('');
+ } else if (!aData.is_playlist) {
+ $(nRow).find("td.library_is_playlist").html('');
+ }
+
// add the play function to the library_type td
$(nRow).find('td.library_type').click(function(){
if (aData.ftype === 'playlist' && aData.length !== '0.0'){
- playlistIndex = $(this).parent().attr('id').substring(3);
- open_playlist_preview(playlistIndex, 0);
+ open_playlist_preview(aData.audioFile, 0);
} else if (aData.ftype === 'audioclip') {
if (isAudioSupported(aData.mime)) {
open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name);
@@ -569,8 +641,7 @@ var AIRTIME = (function(AIRTIME) {
open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name);
}
} else if (aData.ftype == 'block' && aData.bl_type == 'static') {
- blockIndex = $(this).parent().attr('id').substring(3);
- open_block_preview(blockIndex, 0);
+ open_block_preview(aData.audioFile, 0);
}
return false;
});
@@ -605,7 +676,28 @@ var AIRTIME = (function(AIRTIME) {
}
return false;
});
-
+
+ /*$(nRow).find(".media-item-in-use").qtip({
+ content: {
+ text: aData.status_msg
+ },
+ hide: {
+ delay: 500,
+ fixed: true
+ },
+ style: {
+ border: {
+ width: 0,
+ radius: 4
+ },
+ classes: "ui-tooltip-dark ui-tooltip-rounded"
+ },
+ position: {
+ my: "left bottom",
+ at: "right center"
+ },
+ });*/
+
// add a tool tip to appear when the user clicks on the type
// icon.
$(nRow).find("td:not(.library_checkbox, .library_type)").qtip({
@@ -702,8 +794,19 @@ var AIRTIME = (function(AIRTIME) {
$simpleSearch.addClass("sp-invisible");
}
else {
- //clear the advanced search fields and reset datatable
- $(".filter_column input").val("").keyup();
+ // clear the advanced search fields
+ var divs = $("div#advanced_search").children(':visible');
+ $.each(divs, function(i, div){
+ fields = $(div).children().find('input');
+ $.each(fields, function(i, field){
+ if ($(field).val() !== "") {
+ $(field).val("");
+ // we need to reset the results when removing
+ // an advanced search field
+ $(field).keyup();
+ }
+ });
+ });
//reset datatable with previous simple search results (if any)
$(".dataTables_filter input").val(simpleSearchText).keyup();
@@ -757,8 +860,7 @@ var AIRTIME = (function(AIRTIME) {
});
checkImportStatus();
- setInterval(checkImportStatus, 5000);
- setInterval(checkLibrarySCUploadStatus, 5000);
+ checkLibrarySCUploadStatus();
addQtipToSCIcons();
@@ -986,6 +1088,7 @@ function checkImportStatus() {
}
div.hide();
}
+ setTimeout(checkImportStatus, 5000);
});
}
@@ -1019,6 +1122,7 @@ function checkLibrarySCUploadStatus(){
else if (json.sc_id == "-3") {
span.removeClass("progress").addClass("sc-error");
}
+ setTimeout(checkLibrarySCUploadStatus, 5000);
}
function checkSCUploadStatusRequest() {
@@ -1252,6 +1356,8 @@ var validationTypes = {
"composer" : "s",
"conductor" : "s",
"copyright" : "s",
+ "cuein" : "l",
+ "cueout" : "l",
"encoded_by" : "s",
"utime" : "t",
"mtime" : "t",
@@ -1283,12 +1389,23 @@ $(document).ready(function() {
data = $("#edit-md-dialog form").serializeArray();
$.post(baseUrl+'library/edit-file-md', {format: "json", id: file_id, data: data}, function() {
$("#edit-md-dialog").dialog().remove();
- oTable.fnStandingRedraw();
+
+ // don't redraw the library table if we are on calendar page
+ // we would be on calendar if viewing recorded file metadata
+ if ($("#schedule_calendar").length === 0) {
+ oTable.fnStandingRedraw();
+ }
});
});
$('#editmdcancel').live("click", function() {
$("#edit-md-dialog").dialog().remove();
});
+
+ $('#edit-md-dialog').live("keyup", function(event) {
+ if (event.keyCode === 13) {
+ $('#editmdsave').click();
+ }
+ });
});
diff --git a/airtime_mvc/public/js/airtime/library/plupload.js b/airtime_mvc/public/js/airtime/library/plupload.js
index b429c5602..91fdc63ee 100644
--- a/airtime_mvc/public/js/airtime/library/plupload.js
+++ b/airtime_mvc/public/js/airtime/library/plupload.js
@@ -30,8 +30,7 @@ $(document).ready(function() {
var tempFileName = j.tempfilepath;
$.get(baseUrl+'Plupload/copyfile/format/json/name/'+
encodeURIComponent(file.name)+'/tempname/' +
- encodeURIComponent(tempFileName), function(json){
- var jr = jQuery.parseJSON(json);
+ encodeURIComponent(tempFileName), function(jr){
if(jr.error !== undefined) {
var row = $("
")
.append('
' + file.name +'
')
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 2839ab45d..93d1cd80d 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -12,6 +12,7 @@ var AIRTIME = (function(AIRTIME){
viewport,
$lib,
$pl,
+ $togglePl = $(""),
widgetHeight,
resizeTimeout,
width;
@@ -363,6 +364,17 @@ var AIRTIME = (function(AIRTIME){
removeButtonCheck();
}
+ function openPlaylistPanel() {
+ var screenWidth = Math.floor(viewport.width - 40);
+ viewport = AIRTIME.utilities.findViewportDimensions();
+ widgetHeight = viewport.height - 185;
+
+ $lib.width(Math.floor(screenWidth * 0.53));
+ $pl.show().width(Math.floor(screenWidth * 0.44));
+ $pl.height(widgetHeight);
+ $("#pl_edit").hide();
+ }
+
//Purpose of this function is to iterate over all playlist elements
//and verify whether they can be previewed by the browser or not. If not
//then the playlist element is greyed out
@@ -450,9 +462,8 @@ var AIRTIME = (function(AIRTIME){
if ($(this).hasClass('close')) {
var sUrl = baseUrl+"playlist/get-block-info";
mod.disableUI();
- $.post(sUrl, {format:"json", id:blockId}, function(json){
+ $.post(sUrl, {format:"json", id:blockId}, function(data){
$html = "";
- var data = $.parseJSON(json);
var isStatic = data.isStatic;
delete data.type;
if (isStatic) {
@@ -643,8 +654,7 @@ var AIRTIME = (function(AIRTIME){
obj_id = $('input[id="obj_id"]').val();
url = baseUrl+"Playlist/shuffle";
enableLoadingIcon();
- $.post(url, {format: "json", obj_id: obj_id}, function(data){
- var json = $.parseJSON(data)
+ $.post(url, {format: "json", obj_id: obj_id}, function(json){
if (json.error !== undefined) {
alert(json.error);
@@ -711,7 +721,40 @@ var AIRTIME = (function(AIRTIME){
});
-
+
+ $lib.on("click", "#pl_edit", function() {
+ openPlaylistPanel();
+ $.ajax( {
+ url : baseUrl+"usersettings/set-library-screen-settings",
+ type : "POST",
+ data : {
+ settings : {
+ playlist : true
+ },
+ format : "json"
+ },
+ dataType : "json"
+ });
+ });
+
+ $pl.on("click", "#lib_pl_close", function() {
+ var screenWidth = Math.floor(viewport.width - 40);
+ $pl.hide();
+ $lib.width(screenWidth).find("#library_display_length").append($togglePl.show());
+
+ $.ajax( {
+ url : baseUrl+"usersettings/set-library-screen-settings",
+ type : "POST",
+ data : {
+ settings : {
+ playlist : false
+ },
+ format : "json"
+ },
+ dataType : "json"
+ });
+ });
+
$('#save_button').live("click", function(event){
/* Smart blocks: get name, description, and criteria
* Playlists: get name, description
@@ -727,8 +770,7 @@ var AIRTIME = (function(AIRTIME){
enableLoadingIcon();
$.post(save_action,
{format: "json", data: criteria, name: block_name, description: block_desc, obj_id: obj_id, type: obj_type, modified: lastMod},
- function(data){
- var json = $.parseJSON(data);
+ function(json){
if (json.error !== undefined) {
alert(json.error);
}
@@ -737,7 +779,7 @@ var AIRTIME = (function(AIRTIME){
}
setModified(json.modified);
if (obj_type == "block") {
- callback(data, "save");
+ callback(json, "save");
} else {
$('.success').text($.i18n._('Playlist saved'));
$('.success').show();
@@ -749,6 +791,12 @@ var AIRTIME = (function(AIRTIME){
}
);
});
+
+ $("#pl-bl-clear-content").live("click", function(event) {
+ var sUrl = baseUrl+"playlist/empty-content",
+ oData = {};
+ playlistRequest(sUrl, oData);
+ });
}
function setUpPlaylist() {
@@ -884,7 +932,9 @@ var AIRTIME = (function(AIRTIME){
};
mod.fnEdit = function(id, type, url) {
-
+ if ($pl.is(":hidden")) {
+ openPlaylistPanel();
+ }
stopAudioPreview();
$.post(url,
@@ -1049,31 +1099,45 @@ var AIRTIME = (function(AIRTIME){
};
function setWidgetSize() {
- viewport = AIRTIME.utilities.findViewportDimensions();
- widgetHeight = viewport.height - 185;
- width = Math.floor(viewport.width - 80);
-
- var libTableHeight = widgetHeight - 130;
+ viewport = AIRTIME.utilities.findViewportDimensions();
+ widgetHeight = viewport.height - 185;
+ width = Math.floor(viewport.width - 80);
- $lib.height(widgetHeight)
- .find(".dataTables_scrolling")
- .css("max-height", libTableHeight)
- .end()
- .width(Math.floor(width * 0.55));
-
- $pl.height(widgetHeight)
- .width(Math.floor(width * 0.45));
+ var libTableHeight = widgetHeight - 130;
+
+ if (!$pl.is(':hidden')) {
+ $lib.height(widgetHeight)
+ .find(".dataTables_scrolling")
+ .css("max-height", libTableHeight)
+ .end()
+ .width(Math.floor(width * 0.55));
+
+ $pl.height(widgetHeight)
+ .width(Math.floor(width * 0.45));
+ } else {
+ $lib.height(widgetHeight)
+ .find(".dataTables_scrolling")
+ .css("max-height", libTableHeight)
+ .end()
+ .width(width + 40);
+ }
}
mod.onReady = function() {
$lib = $("#library_content");
$pl = $("#side_playlist");
+
+
setWidgetSize();
AIRTIME.library.libraryInit();
AIRTIME.playlist.init();
-
+
+ if ($pl.is(':hidden')) {
+ $lib.find("#library_display_length").append($togglePl.show());
+ }
+
$pl.find(".ui-icon-alert").qtip({
content: {
text: $.i18n._("Airtime is unsure about the status of this file. This can happen when the file is on a remote drive that is unaccessible or the file is in a directory that isn't 'watched' anymore.")
diff --git a/airtime_mvc/public/js/airtime/listenerstat/listenerstat.js b/airtime_mvc/public/js/airtime/listenerstat/listenerstat.js
index 926368966..bebfd1471 100644
--- a/airtime_mvc/public/js/airtime/listenerstat/listenerstat.js
+++ b/airtime_mvc/public/js/airtime/listenerstat/listenerstat.js
@@ -23,7 +23,6 @@ $(document).ready(function() {
function getDataAndPlot(startTimestamp, endTimestamp){
// get data
$.get(baseUrl+'Listenerstat/get-data', {startTimestamp: startTimestamp, endTimestamp: endTimestamp}, function(data){
- data = JSON.parse(data);
out = new Object();
$.each(data, function(mpName, v){
plotData = new Object();
diff --git a/airtime_mvc/public/js/airtime/login/login.js b/airtime_mvc/public/js/airtime/login/login.js
index a37210b35..f21043c31 100644
--- a/airtime_mvc/public/js/airtime/login/login.js
+++ b/airtime_mvc/public/js/airtime/login/login.js
@@ -1,6 +1,6 @@
$(window).load(function(){
$("#username").focus();
- $("#locale").val($.cookie("airtime_locale")!== null?$.cookie("airtime_locale"):'en_CA');
+ $("#locale").val($.cookie("airtime_locale")!== null?$.cookie("airtime_locale"):$.cookie("default_airtime_locale"));
});
$(document).ready(function() {
diff --git a/airtime_mvc/public/js/airtime/login/password-restore.js b/airtime_mvc/public/js/airtime/login/password-restore.js
index 04d78f98b..4b45a59ff 100644
--- a/airtime_mvc/public/js/airtime/login/password-restore.js
+++ b/airtime_mvc/public/js/airtime/login/password-restore.js
@@ -1,3 +1,3 @@
function redirectToLogin(){
- window.location = baseUrl+"/Login"
+ window.location = baseUrl+"Login"
}
\ No newline at end of file
diff --git a/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js b/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js
index 68bb71fd5..f47a7b8ed 100644
--- a/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js
+++ b/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js
@@ -351,7 +351,7 @@ function setupUI() {
* It is only active if playlist is not empty
*/
var plContents = $('#spl_sortable').children();
- var shuffleButton = $('button[id="shuffle_button"], button[id="playlist_shuffle_button"]');
+ var shuffleButton = $('button[id="shuffle_button"], button[id="playlist_shuffle_button"], button[id="pl-bl-clear-content"]');
if (!plContents.hasClass('spl_empty')) {
if (shuffleButton.hasClass('ui-state-disabled')) {
@@ -480,9 +480,8 @@ function getCriteriaOptionType(e) {
return criteriaTypes[criteria];
}
-function callback(data, type) {
- var json = $.parseJSON(data),
- dt = $('table[id="library_display"]').dataTable();
+function callback(json, type) {
+ var dt = $('table[id="library_display"]').dataTable();
if (type == 'shuffle' || type == 'generate') {
if (json.error !== undefined) {
@@ -560,7 +559,9 @@ function enableLoadingIcon() {
function disableLoadingIcon() {
$("#side_playlist").unblock()
}
-
+// We need to know if the criteria value will be a string
+// or numeric value in order to populate the modifier
+// select list
var criteriaTypes = {
0 : "",
"album_title" : "s",
@@ -569,6 +570,8 @@ var criteriaTypes = {
"composer" : "s",
"conductor" : "s",
"copyright" : "s",
+ "cuein" : "n",
+ "cueout" : "n",
"artist_name" : "s",
"encoded_by" : "s",
"utime" : "n",
diff --git a/airtime_mvc/public/js/airtime/preferences/preferences.js b/airtime_mvc/public/js/airtime/preferences/preferences.js
index 1d529617d..50466f462 100644
--- a/airtime_mvc/public/js/airtime/preferences/preferences.js
+++ b/airtime_mvc/public/js/airtime/preferences/preferences.js
@@ -108,9 +108,9 @@ $(document).ready(function() {
var data = $('#pref_form').serialize();
var url = baseUrl+'Preference/index';
- $.post(url, {format: "json", data: data}, function(data){
- var json = $.parseJSON(data);
+ $.post(url, {format: "json", data: data}, function(json){
$('#content').empty().append(json.html);
+ $.cookie("default_airtime_locale", $('#locale').val(), {path: '/'});
setTimeout(removeSuccessMsg, 5000);
showErrorSections();
});
diff --git a/airtime_mvc/public/js/airtime/preferences/streamsetting.js b/airtime_mvc/public/js/airtime/preferences/streamsetting.js
index 54bb986ca..01d4717b3 100644
--- a/airtime_mvc/public/js/airtime/preferences/streamsetting.js
+++ b/airtime_mvc/public/js/airtime/preferences/streamsetting.js
@@ -28,7 +28,7 @@ function rebuildStreamURL(ele){
}else{
streamurl = "http://"+host+":"+port+"/"
}
- div.find("#stream_url").html(streamurl)
+ div.find("#stream_url").text(streamurl)
}
function restrictOggBitrate(ele, on){
var div = ele.closest("div")
@@ -71,14 +71,13 @@ function showForIcecast(ele){
div.find("#outputMountpoint-element").show()
div.find("#outputUser-label").show()
div.find("#outputUser-element").show()
- div.find("select[id$=data-type]").find("option[value='ogg']").attr("disabled","");
+ div.find("select[id$=data-type]").find("option[value='ogg']").removeAttr("disabled");
}
function checkLiquidsoapStatus(){
var url = baseUrl+'Preference/get-liquidsoap-status/format/json';
var id = $(this).attr("id");
- $.post(url, function(json){
- var json_obj = jQuery.parseJSON(json);
+ $.post(url, function(json_obj){
for(var i=0;i');
- } else if ((view.name === 'agendaDay' || view.name === 'agendaWeek') && event.record === 1 && event.soundcloud_id > 0) {
- $(element).find(".fc-event-time").before('');
- } else if ((view.name === 'agendaDay' || view.name === 'agendaWeek') && event.record === 1 && event.soundcloud_id === -2) {
- $(element).find(".fc-event-time").before('');
- } else if ((view.name === 'agendaDay' || view.name === 'agendaWeek') && event.record === 1 && event.soundcloud_id === -3) {
- $(element).find(".fc-event-time").before('');
- }
-
- if(view.name === 'month' && event.record === 1 && event.soundcloud_id === -1) {
- $(element).find(".fc-event-title").after('');
- } else if (view.name === 'month' && event.record === 1 && event.soundcloud_id > 0) {
- $(element).find(".fc-event-title").after('');
- } else if (view.name === 'month' && event.record === 1 && event.soundcloud_id === -2) {
- $(element).find(".fc-event-title").after('');
- } else if (view.name === 'month' && event.record === 1 && event.soundcloud_id === -3) {
- $(element).find(".fc-event-title").after('');
- }
-
- if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
- if (event.show_empty === 1 && event.record === 0 && event.rebroadcast === 0) {
- $(element)
- .find(".fc-event-time")
- .before('');
- } else if (event.show_partial_filled === true) {
- $(element)
- .find(".fc-event-time")
- .before('');
+ if (event.record === 1) {
+ if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
+ if (event.soundcloud_id === -1) {
+ $(element).find(".fc-event-time").before('');
+ } else if ( event.soundcloud_id > 0) {
+ $(element).find(".fc-event-time").before('');
+ } else if (event.soundcloud_id === -2) {
+ $(element).find(".fc-event-time").before('');
+ } else if (event.soundcloud_id === -3) {
+ $(element).find(".fc-event-time").before('');
+ }
+ } else if (view.name === 'month') {
+ if(event.soundcloud_id === -1) {
+ $(element).find(".fc-event-title").after('');
+ } else if (event.soundcloud_id > 0) {
+ $(element).find(".fc-event-title").after('');
+ } else if (event.soundcloud_id === -2) {
+ $(element).find(".fc-event-title").after('');
+ } else if (event.soundcloud_id === -3) {
+ $(element).find(".fc-event-title").after('');
+ }
}
- } else if (view.name === 'month') {
- if (event.show_empty === 1 && event.record === 0 && event.rebroadcast === 0) {
- $(element)
- .find(".fc-event-title")
- .after('');
- } else if (event.show_partial_filled === true) {
- $(element)
- .find(".fc-event-title")
- .after('');
+ }
+
+ if (event.record === 0 && event.rebroadcast === 0) {
+ if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
+ if (event.show_empty === 1) {
+ $(element)
+ .find(".fc-event-time")
+ .before('');
+ } else if (event.show_partial_filled === true) {
+ $(element)
+ .find(".fc-event-time")
+ .before('');
+ }
+ } else if (view.name === 'month') {
+ if (event.show_empty === 1) {
+ $(element)
+ .find(".fc-event-title")
+ .after('');
+ } else if (event.show_partial_filled === true) {
+ $(element)
+ .find(".fc-event-title")
+ .after('');
+ }
}
}
//rebroadcast icon
- if((view.name === 'agendaDay' || view.name === 'agendaWeek') && event.rebroadcast === 1) {
- $(element).find(".fc-event-time").before('');
- }
-
- if(view.name === 'month' && event.rebroadcast === 1) {
- $(element).find(".fc-event-title").after('');
+ if (event.rebroadcast === 1) {
+ if (view.name === 'agendaDay' || view.name === 'agendaWeek') {
+ $(element).find(".fc-event-time").before('');
+ } else if (view.name === 'month') {
+ $(element).find(".fc-event-title").after('');
+ }
}
}
@@ -326,21 +336,36 @@ function eventResize( event, dayDelta, minuteDelta, revertFunc, jsEvent, ui, vie
});
}
-function getFullCalendarEvents(start, end, callback) {
- var url, start_date, end_date;
-
- start_date = makeTimeStamp(start);
- end_date = makeTimeStamp(end);
-
- url = baseUrl+'Schedule/event-feed';
-
+function preloadEventFeed () {
+ var url = baseUrl+'Schedule/event-feed-preload';
var d = new Date();
-
- $.post(url, {format: "json", start: start_date, end: end_date, cachep: d.getTime()}, function(json){
- callback(json.events);
+
+ $.post(url, {format: "json", cachep: d.getTime()}, function(json){
+ calendarEvents = json.events;
+ createFullCalendar({calendarInit: calendarPref});
});
}
+var initialLoad = true;
+function getFullCalendarEvents(start, end, callback) {
+
+ if (initialLoad) {
+ initialLoad = false;
+ callback(calendarEvents);
+ } else {
+ var url, start_date, end_date;
+
+ start_date = makeTimeStamp(start);
+ end_date = makeTimeStamp(end);
+ url = baseUrl+'Schedule/event-feed';
+
+ var d = new Date();
+ $.post(url, {format: "json", start: start_date, end: end_date, cachep: d.getTime()}, function(json){
+ callback(json.events);
+ });
+ }
+}
+
function checkSCUploadStatus(){
var url = baseUrl+'Library/get-upload-to-soundcloud-status/format/json';
$("span[class*=progress]").each(function(){
@@ -351,6 +376,7 @@ function checkSCUploadStatus(){
}else if(json.sc_id == "-3"){
$("span[id="+id+"]:not(.recording)").removeClass("progress").addClass("sc-error");
}
+ setTimeout(checkSCUploadStatus, 5000);
});
});
}
@@ -403,6 +429,7 @@ function getCurrentShow(){
$(this).remove("span[small-icon now-playing]");
}
});
+ setTimeout(getCurrentShow, 5000);
});
}
@@ -541,9 +568,10 @@ function alertShowErrorAndReload(){
window.location.reload();
}
+preloadEventFeed();
$(document).ready(function(){
- setInterval( "checkSCUploadStatus()", 5000 );
- setInterval( "getCurrentShow()", 5000 );
+ checkSCUploadStatus();
+ getCurrentShow();
});
var view_name;
diff --git a/airtime_mvc/public/js/airtime/schedule/schedule.js b/airtime_mvc/public/js/airtime/schedule/schedule.js
index 2de53cdb5..57dc8def5 100644
--- a/airtime_mvc/public/js/airtime/schedule/schedule.js
+++ b/airtime_mvc/public/js/airtime/schedule/schedule.js
@@ -93,6 +93,8 @@ function checkCalendarSCUploadStatus(){
else if (json.sc_id == "-3") {
span.removeClass("progress").addClass("sc-error");
}
+
+ setTimeout(checkCalendarSCUploadStatus, 5000);
}
function checkSCUploadStatusRequest() {
@@ -328,10 +330,7 @@ function alertShowErrorAndReload(){
}
$(document).ready(function() {
- $.ajax({ url: baseUrl+"Api/calendar-init/format/json", dataType:"json", success:createFullCalendar
- , error:function(jqXHR, textStatus, errorThrown){}});
-
- setInterval(checkCalendarSCUploadStatus, 5000);
+ checkCalendarSCUploadStatus();
$.contextMenu({
selector: 'div.fc-event',
@@ -402,7 +401,7 @@ $(document).ready(function() {
oItems.edit.callback = callback;
}
}
-
+
//define a content callback.
if (oItems.content !== undefined) {
@@ -443,9 +442,11 @@ $(document).ready(function() {
//define a view recorded callback.
if (oItems.view_recorded !== undefined) {
-
callback = function() {
- document.location.href = oItems.view_recorded.url;
+ $.get(oItems.view_recorded.url, {format: "json"}, function(json){
+ //in library.js
+ buildEditMetadataDialog(json);
+ });
};
oItems.view_recorded.callback = callback;
}
diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js
index b7d4bda7f..9bbc4db92 100644
--- a/airtime_mvc/public/js/airtime/showbuilder/builder.js
+++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js
@@ -81,9 +81,20 @@ var AIRTIME = (function(AIRTIME){
return mod.showInstances;
};
- mod.refresh = function() {
+ mod.refresh = function(schedId) {
mod.resetTimestamp();
- oSchedTable.fnDraw();
+
+ // once a track plays out we need to check if we can update
+ // the is_scheduled flag in cc_files
+ if (schedId > 0) {
+ $.post(baseUrl+"schedule/update-future-is-scheduled",
+ {"format": "json", "schedId": schedId}, function(data) {
+ if (data.redrawLibTable !== undefined && data.redrawLibTable) {
+ $("#library_content").find("#library_display").dataTable().fnStandingRedraw();
+ }
+ });
+ oSchedTable.fnDraw();
+ }
};
mod.checkSelectButton = function() {
@@ -251,10 +262,11 @@ var AIRTIME = (function(AIRTIME){
mod.fnItemCallback = function(json) {
checkError(json);
- mod.getSelectedCursors();
+ mod.getSelectedCursors();
oSchedTable.fnDraw();
-
+
mod.enableUI();
+ $("#library_content").find("#library_display").dataTable().fnStandingRedraw();
};
mod.getSelectedCursors = function() {
@@ -796,7 +808,7 @@ var AIRTIME = (function(AIRTIME){
if(refreshInterval > maxRefreshInterval){
refreshInterval = maxRefreshInterval;
}
- mod.timeout = setTimeout(mod.refresh, refreshInterval); //need refresh in milliseconds
+ mod.timeout = setTimeout(function() {mod.refresh(aData.id)}, refreshInterval); //need refresh in milliseconds
break;
}
}
@@ -1066,6 +1078,7 @@ var AIRTIME = (function(AIRTIME){
url: url,
data: {format: "json", id: data.instance},
success: function(data){
+ $("#library_content").find("#library_display").dataTable().fnStandingRedraw();
var oTable = $sbTable.dataTable();
oTable.fnDraw();
}
diff --git a/airtime_mvc/public/js/airtime/showbuilder/main_builder.js b/airtime_mvc/public/js/airtime/showbuilder/main_builder.js
index 214bc9714..ee19e85fd 100644
--- a/airtime_mvc/public/js/airtime/showbuilder/main_builder.js
+++ b/airtime_mvc/public/js/airtime/showbuilder/main_builder.js
@@ -86,7 +86,7 @@ AIRTIME = (function(AIRTIME) {
.end();
oTable = $('#show_builder_table').dataTable();
- oTable.fnDraw();
+ //oTable.fnDraw();
}
}
@@ -277,12 +277,13 @@ AIRTIME = (function(AIRTIME) {
if (json.update === true) {
oTable.fnDraw();
}
+ setTimeout(checkScheduleUpdates, 5000);
}
});
}
//check if the timeline view needs updating.
- setInterval(checkScheduleUpdates, 5 * 1000); //need refresh in milliseconds
+ checkScheduleUpdates();
};
mod.onResize = function() {
diff --git a/airtime_mvc/public/js/airtime/status/status.js b/airtime_mvc/public/js/airtime/status/status.js
index 9b5e4d3ea..ef3358e7a 100644
--- a/airtime_mvc/public/js/airtime/status/status.js
+++ b/airtime_mvc/public/js/airtime/status/status.js
@@ -66,6 +66,7 @@ function success(data, textStatus, jqXHR){
if (data.status.partitions){
generatePartitions(data.status.partitions);
}
+ setTimeout(function(){updateStatus(false);}, 5000);
}
function updateStatus(getDiskInfo){
@@ -75,5 +76,4 @@ function updateStatus(getDiskInfo){
$(document).ready(function() {
updateStatus(true);
- setInterval(function(){updateStatus(false);}, 5000);
});
diff --git a/airtime_mvc/public/js/airtime/user/user.js b/airtime_mvc/public/js/airtime/user/user.js
index fd129be79..71f9eed6e 100644
--- a/airtime_mvc/public/js/airtime/user/user.js
+++ b/airtime_mvc/public/js/airtime/user/user.js
@@ -189,8 +189,7 @@ $(document).ready(function() {
var data = $('#user_form').serialize();
var url = baseUrl+'User/add-user';
- $.post(url, {format: "json", data: data}, function(data){
- var json = $.parseJSON(data);
+ $.post(url, {format: "json", data: data}, function(json){
if (json.valid === "true") {
$('#content').empty().append(json.html);
populateUserTable();
diff --git a/airtime_mvc/public/js/datatables/i18n/el_GR.txt b/airtime_mvc/public/js/datatables/i18n/el_GR.txt
new file mode 100644
index 000000000..03f52058f
--- /dev/null
+++ b/airtime_mvc/public/js/datatables/i18n/el_GR.txt
@@ -0,0 +1,18 @@
+// Greek
+{
+ "sProcessing": "Επεξεργασία...",
+ "sLengthMenu": "Δείξε _MENU_ εγγραφές",
+ "sZeroRecords": "Δεν βρέθηκαν εγγραφές που να ταιριάζουν",
+ "sInfo": "Δείχνοντας _START_ εως _END_ από _TOTAL_ εγγραφές",
+ "sInfoEmpty": "Δείχνοντας 0 εως 0 από 0 εγγραφές",
+ "sInfoFiltered": "(φιλτραρισμένες από _MAX_ συνολικά εγγραφές)",
+ "sInfoPostFix": "",
+ "sSearch": "",
+ "sUrl": "",
+ "oPaginate": {
+ "sFirst": "Πρώτη",
+ "sPrevious": "Προηγούμενη",
+ "sNext": "Επόμενη",
+ "sLast": "Τελευταία"
+ }
+}
\ No newline at end of file
diff --git a/airtime_mvc/public/js/datatables/i18n/pl_PL.txt b/airtime_mvc/public/js/datatables/i18n/pl_PL.txt
new file mode 100644
index 000000000..5d73fcc24
--- /dev/null
+++ b/airtime_mvc/public/js/datatables/i18n/pl_PL.txt
@@ -0,0 +1,18 @@
+//Polish
+{
+ "sProcessing": "Proszę czekać...",
+ "sLengthMenu": "Pokaż _MENU_ pozycji",
+ "sZeroRecords": "Nie znaleziono żadnych pasujących indeksów",
+ "sInfo": "Pozycje od _START_ do _END_ z _TOTAL_ łącznie",
+ "sInfoEmpty": "Pozycji 0 z 0 dostępnych",
+ "sInfoFiltered": "(filtrowanie spośród _MAX_ dostępnych pozycji)",
+ "sInfoPostFix": "",
+ "sSearch": "",
+ "sUrl": "",
+ "oPaginate": {
+ "sFirst": "Pierwsza",
+ "sPrevious": "Poprzednia",
+ "sNext": "Następna",
+ "sLast": "Ostatnia"
+ }
+}
\ No newline at end of file
diff --git a/airtime_mvc/public/js/datatables/js/jquery.dataTables.js b/airtime_mvc/public/js/datatables/js/jquery.dataTables.js
index ab61ea58e..02694a4a5 100644
--- a/airtime_mvc/public/js/datatables/js/jquery.dataTables.js
+++ b/airtime_mvc/public/js/datatables/js/jquery.dataTables.js
@@ -1,12 +1,10 @@
-/**
- * @summary DataTables
- * @description Paginate, search and sort HTML tables
- * @version 1.9.1
- * @file jquery.dataTables.js
- * @author Allan Jardine (www.sprymedia.co.uk)
- * @contact www.sprymedia.co.uk/contact
- *
- * @copyright Copyright 2008-2012 Allan Jardine, all rights reserved.
+/*
+ * File: jquery.dataTables.min.js
+ * Version: 1.9.4
+ * Author: Allan Jardine (www.sprymedia.co.uk)
+ * Info: www.datatables.net
+ *
+ * Copyright 2008-2012 Allan Jardine, all rights reserved.
*
* This source file is free software, under either the GPL v2 license or a
* BSD style license, available at:
@@ -16,11823 +14,142 @@
* This source file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
- *
- * For details please refer to: http://www.datatables.net
*/
-
-/*jslint evil: true, undef: true, browser: true */
-/*globals $, jQuery,_fnExternApiFunc,_fnInitialise,_fnInitComplete,_fnLanguageCompat,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnCreateTr,_fnGatherData,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnServerParams,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAdjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnDetectHeader,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap,_fnGetRowData,_fnGetCellData,_fnSetCellData,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnApplyColumnDefs,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnJsonString,_fnRender,_fnNodeToColumnIndex,_fnInfoMacros*/
-
-(/** @lends */function($, window, document, undefined) {
- /**
- * DataTables is a plug-in for the jQuery Javascript library. It is a
- * highly flexible tool, based upon the foundations of progressive
- * enhancement, which will add advanced interaction controls to any
- * HTML table. For a full list of features please refer to
- * DataTables.net.
- *
- * Note that the DataTable object is not a global variable but is
- * aliased to jQuery.fn.DataTable and jQuery.fn.dataTable through which
- * it may be accessed.
- *
- * @class
- * @param {object} [oInit={}] Configuration object for DataTables. Options
- * are defined by {@link DataTable.defaults}
- * @requires jQuery 1.3+
- *
- * @example
- * // Basic initialisation
- * $(document).ready( function {
- * $('#example').dataTable();
- * } );
- *
- * @example
- * // Initialisation with configuration options - in this case, disable
- * // pagination and sorting.
- * $(document).ready( function {
- * $('#example').dataTable( {
- * "bPaginate": false,
- * "bSort": false
- * } );
- * } );
- */
- var DataTable = function( oInit )
- {
-
-
- /**
- * Add a column to the list used for the table with default values
- * @param {object} oSettings dataTables settings object
- * @param {node} nTh The th element for this column
- * @memberof DataTable#oApi
- */
- function _fnAddColumn( oSettings, nTh )
- {
- var oDefaults = DataTable.defaults.columns;
- var iCol = oSettings.aoColumns.length;
- var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, {
- "sSortingClass": oSettings.oClasses.sSortable,
- "sSortingClassJUI": oSettings.oClasses.sSortJUI,
- "nTh": nTh ? nTh : document.createElement('th'),
- "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '',
- "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol],
- "mDataProp": oDefaults.mDataProp ? oDefaults.oDefaults : iCol
- } );
- oSettings.aoColumns.push( oCol );
-
- /* Add a column specific filter */
- if ( oSettings.aoPreSearchCols[ iCol ] === undefined || oSettings.aoPreSearchCols[ iCol ] === null )
- {
- oSettings.aoPreSearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch );
- }
- else
- {
- var oPre = oSettings.aoPreSearchCols[ iCol ];
-
- /* Don't require that the user must specify bRegex, bSmart or bCaseInsensitive */
- if ( oPre.bRegex === undefined )
- {
- oPre.bRegex = true;
- }
-
- if ( oPre.bSmart === undefined )
- {
- oPre.bSmart = true;
- }
-
- if ( oPre.bCaseInsensitive === undefined )
- {
- oPre.bCaseInsensitive = true;
- }
- }
-
- /* Use the column options function to initialise classes etc */
- _fnColumnOptions( oSettings, iCol, null );
- }
-
-
- /**
- * Apply options for a column
- * @param {object} oSettings dataTables settings object
- * @param {int} iCol column index to consider
- * @param {object} oOptions object with sType, bVisible and bSearchable
- * @memberof DataTable#oApi
- */
- function _fnColumnOptions( oSettings, iCol, oOptions )
- {
- var oCol = oSettings.aoColumns[ iCol ];
-
- /* User specified column options */
- if ( oOptions !== undefined && oOptions !== null )
- {
- if ( oOptions.sType !== undefined )
- {
- oCol.sType = oOptions.sType;
- oCol._bAutoType = false;
- }
-
- $.extend( oCol, oOptions );
- _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );
-
- /* iDataSort to be applied (backwards compatibility), but aDataSort will take
- * priority if defined
- */
- if ( oOptions.iDataSort !== undefined )
- {
- oCol.aDataSort = [ oOptions.iDataSort ];
- }
- _fnMap( oCol, oOptions, "aDataSort" );
- }
-
- /* Cache the data get and set functions for speed */
- oCol.fnGetData = _fnGetObjectDataFn( oCol.mDataProp );
- oCol.fnSetData = _fnSetObjectDataFn( oCol.mDataProp );
-
- /* Feature sorting overrides column specific when off */
- if ( !oSettings.oFeatures.bSort )
- {
- oCol.bSortable = false;
- }
-
- /* Check that the class assignment is correct for sorting */
- if ( !oCol.bSortable ||
- ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )
- {
- oCol.sSortingClass = oSettings.oClasses.sSortableNone;
- oCol.sSortingClassJUI = "";
- }
- else if ( oCol.bSortable ||
- ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )
- {
- oCol.sSortingClass = oSettings.oClasses.sSortable;
- oCol.sSortingClassJUI = oSettings.oClasses.sSortJUI;
- }
- else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 )
- {
- oCol.sSortingClass = oSettings.oClasses.sSortableAsc;
- oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;
- }
- else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 )
- {
- oCol.sSortingClass = oSettings.oClasses.sSortableDesc;
- oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;
- }
- }
-
-
- /**
- * Adjust the table column widths for new data. Note: you would probably want to
- * do a redraw after calling this function!
- * @param {object} oSettings dataTables settings object
- * @memberof DataTable#oApi
- */
- function _fnAdjustColumnSizing ( oSettings )
- {
- /* Not interested in doing column width calculation if autowidth is disabled */
- if ( oSettings.oFeatures.bAutoWidth === false )
- {
- return false;
- }
-
- _fnCalculateColumnWidths( oSettings );
- for ( var i=0 , iLen=oSettings.aoColumns.length ; i=0 ; i-- )
- {
- /* Each definition can target multiple columns, as it is an array */
- var aTargets = aoColDefs[i].aTargets;
- if ( !$.isArray( aTargets ) )
- {
- _fnLog( oSettings, 1, 'aTargets must be an array of targets, not a '+(typeof aTargets) );
- }
-
- for ( j=0, jLen=aTargets.length ; j= 0 )
- {
- /* Add columns that we don't yet know about */
- while( oSettings.aoColumns.length <= aTargets[j] )
- {
- _fnAddColumn( oSettings );
- }
-
- /* Integer, basic index */
- fn( aTargets[j], aoColDefs[i] );
- }
- else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 )
- {
- /* Negative integer, right to left column counting */
- fn( oSettings.aoColumns.length+aTargets[j], aoColDefs[i] );
- }
- else if ( typeof aTargets[j] === 'string' )
- {
- /* Class name matching on TH element */
- for ( k=0, kLen=oSettings.aoColumns.length ; k=0 if successful (index of new aoData entry), -1 if failed
- * @memberof DataTable#oApi
- */
- function _fnAddData ( oSettings, aDataSupplied )
- {
- var oCol;
-
- /* Take an independent copy of the data source so we can bash it about as we wish */
- var aDataIn = ($.isArray(aDataSupplied)) ?
- aDataSupplied.slice() :
- $.extend( true, {}, aDataSupplied );
-
- /* Create the object for storing information about this new row */
- var iRow = oSettings.aoData.length;
- var oData = $.extend( true, {}, DataTable.models.oRow );
- oData._aData = aDataIn;
- oSettings.aoData.push( oData );
-
- /* Create the cells */
- var nTd, sThisType;
- for ( var i=0, iLen=oSettings.aoColumns.length ; i iTarget )
- {
- a[i]--;
- }
- }
-
- if ( iTargetIndex != -1 )
- {
- a.splice( iTargetIndex, 1 );
- }
- }
-
-
- /**
- * Call the developer defined fnRender function for a given cell (row/column) with
- * the required parameters and return the result.
- * @param {object} oSettings dataTables settings object
- * @param {int} iRow aoData index for the row
- * @param {int} iCol aoColumns index for the column
- * @returns {*} Return of the developer's fnRender function
- * @memberof DataTable#oApi
- */
- function _fnRender( oSettings, iRow, iCol )
- {
- var oCol = oSettings.aoColumns[iCol];
-
- return oCol.fnRender( {
- "iDataRow": iRow,
- "iDataColumn": iCol,
- "oSettings": oSettings,
- "aData": oSettings.aoData[iRow]._aData,
- "mDataProp": oCol.mDataProp
- }, _fnGetCellData(oSettings, iRow, iCol, 'display') );
- }
-
-
- /**
- * Create a new TR element (and it's TD children) for a row
- * @param {object} oSettings dataTables settings object
- * @param {int} iRow Row to consider
- * @memberof DataTable#oApi
- */
- function _fnCreateTr ( oSettings, iRow )
- {
- var oData = oSettings.aoData[iRow];
- var nTd;
-
- if ( oData.nTr === null )
- {
- oData.nTr = document.createElement('tr');
-
- /* Use a private property on the node to allow reserve mapping from the node
- * to the aoData array for fast look up
- */
- oData.nTr._DT_RowIndex = iRow;
-
- /* Special parameters can be given by the data source to be used on the row */
- if ( oData._aData.DT_RowId )
- {
- oData.nTr.id = oData._aData.DT_RowId;
- }
-
- if ( oData._aData.DT_RowClass )
- {
- $(oData.nTr).addClass( oData._aData.DT_RowClass );
- }
-
- /* Process each column */
- for ( var i=0, iLen=oSettings.aoColumns.length ; i=0 ; j-- )
- {
- if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden )
- {
- aoLocal[i].splice( j, 1 );
- }
- }
-
- /* Prep the applied array - it needs an element for each row */
- aApplied.push( [] );
- }
-
- for ( i=0, iLen=aoLocal.length ; i= oSettings.fnRecordsDisplay()) ?
- 0 : oSettings.iInitDisplayStart;
- }
- oSettings.iInitDisplayStart = -1;
- _fnCalculateEnd( oSettings );
- }
-
- /* Server-side processing draw intercept */
- if ( oSettings.bDeferLoading )
- {
- oSettings.bDeferLoading = false;
- oSettings.iDraw++;
- }
- else if ( !oSettings.oFeatures.bServerSide )
- {
- oSettings.iDraw++;
- }
- else if ( !oSettings.bDestroying && !_fnAjaxUpdate( oSettings ) )
- {
- return;
- }
-
- if ( oSettings.aiDisplay.length !== 0 )
- {
- var iStart = oSettings._iDisplayStart;
- var iEnd = oSettings._iDisplayEnd;
-
- if ( oSettings.oFeatures.bServerSide )
- {
- iStart = 0;
- iEnd = oSettings.aoData.length;
- }
-
- for ( var j=iStart ; j
')[0];
- oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );
-
- /*
- * All DataTables are wrapped in a div
- */
- oSettings.nTableWrapper = $('')[0];
- oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling;
-
- /* Track where we want to insert the option */
- var nInsertNode = oSettings.nTableWrapper;
-
- /* Loop over the user set positioning and place the elements as needed */
- var aDom = oSettings.sDom.split('');
- var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j;
- for ( var i=0 ; i
')[0];
-
- /* Check to see if we should append an id and/or a class name to the container */
- cNext = aDom[i+1];
- if ( cNext == "'" || cNext == '"' )
- {
- sAttr = "";
- j = 2;
- while ( aDom[i+j] != cNext )
- {
- sAttr += aDom[i+j];
- j++;
- }
-
- /* Replace jQuery UI constants */
- if ( sAttr == "H" )
- {
- sAttr = "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix";
- }
- else if ( sAttr == "F" )
- {
- sAttr = "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix";
- }
-
- /* The attribute can be in the format of "#id.class", "#id" or "class" This logic
- * breaks the string into parts and applies them as needed
- */
- if ( sAttr.indexOf('.') != -1 )
- {
- var aSplit = sAttr.split('.');
- nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1);
- nNewNode.className = aSplit[1];
- }
- else if ( sAttr.charAt(0) == "#" )
- {
- nNewNode.id = sAttr.substr(1, sAttr.length-1);
- }
- else
- {
- nNewNode.className = sAttr;
- }
-
- i += j; /* Move along the position array */
- }
-
- nInsertNode.appendChild( nNewNode );
- nInsertNode = nNewNode;
- }
- else if ( cOption == '>' )
- {
- /* End container div */
- nInsertNode = nInsertNode.parentNode;
- }
- else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange )
- {
- /* Length */
- nTmp = _fnFeatureHtmlLength( oSettings );
- iPushFeature = 1;
- }
- else if ( cOption == 'f' && oSettings.oFeatures.bFilter )
- {
- /* Filter */
- nTmp = _fnFeatureHtmlFilter( oSettings );
- iPushFeature = 1;
- }
- else if ( cOption == 'r' && oSettings.oFeatures.bProcessing )
- {
- /* pRocessing */
- nTmp = _fnFeatureHtmlProcessing( oSettings );
- iPushFeature = 1;
- }
- else if ( cOption == 't' )
- {
- /* Table */
- nTmp = _fnFeatureHtmlTable( oSettings );
- iPushFeature = 1;
- }
- else if ( cOption == 'i' && oSettings.oFeatures.bInfo )
- {
- /* Info */
- nTmp = _fnFeatureHtmlInfo( oSettings );
- iPushFeature = 1;
- }
- else if ( cOption == 'p' && oSettings.oFeatures.bPaginate )
- {
- /* Pagination */
- nTmp = _fnFeatureHtmlPaginate( oSettings );
- iPushFeature = 1;
- }
- else if ( DataTable.ext.aoFeatures.length !== 0 )
- {
- /* Plug-in features */
- var aoFeatures = DataTable.ext.aoFeatures;
- for ( var k=0, kLen=aoFeatures.length ; k') :
- sSearchStr==="" ? '' : sSearchStr+' ';
-
- var nFilter = document.createElement( 'div' );
- nFilter.className = oSettings.oClasses.sFilter;
- nFilter.innerHTML = '';
- if ( !oSettings.aanFeatures.f )
- {
- nFilter.id = oSettings.sTableId+'_filter';
- }
-
- var jqFilter = $('input[type="text"]', nFilter);
-
- // Store a reference to the input element, so other input elements could be
- // added to the filter wrapper if needed (submit button for example)
- nFilter._DT_Input = jqFilter[0];
-
- jqFilter.val( oPreviousSearch.sSearch.replace('"','"') );
- jqFilter.bind( 'keyup.DT', function(e) {
- /* Update all other filter input elements for the new display */
- var n = oSettings.aanFeatures.f;
- var val = this.value==="" ? "" : this.value; // mental IE8 fix :-(
-
- for ( var i=0, iLen=n.length ; i=0 ; i-- )
- {
- var sData = _fnDataToSearch( _fnGetCellData( oSettings, oSettings.aiDisplay[i], iColumn, 'filter' ),
- oSettings.aoColumns[iColumn].sType );
- if ( ! rpSearch.test( sData ) )
- {
- oSettings.aiDisplay.splice( i, 1 );
- iIndexCorrector++;
- }
- }
- }
-
-
- /**
- * Filter the data table based on user input and draw the table
- * @param {object} oSettings dataTables settings object
- * @param {string} sInput string to filter on
- * @param {int} iForce optional - force a research of the master array (1) or not (undefined or 0)
- * @param {bool} bRegex treat as a regular expression or not
- * @param {bool} bSmart perform smart filtering or not
- * @param {bool} bCaseInsensitive Do case insenstive matching or not
- * @memberof DataTable#oApi
- */
- function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart, bCaseInsensitive )
- {
- var i;
- var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive );
- var oPrevSearch = oSettings.oPreviousSearch;
-
- /* Check if we are forcing or not - optional parameter */
- if ( !iForce )
- {
- iForce = 0;
- }
-
- /* Need to take account of custom filtering functions - always filter */
- if ( DataTable.ext.afnFiltering.length !== 0 )
- {
- iForce = 1;
- }
-
- /*
- * If the input is blank - we want the full data set
- */
- if ( sInput.length <= 0 )
- {
- oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
- oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
- }
- else
- {
- /*
- * We are starting a new search or the new search string is smaller
- * then the old one (i.e. delete). Search from the master array
- */
- if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||
- oPrevSearch.sSearch.length > sInput.length || iForce == 1 ||
- sInput.indexOf(oPrevSearch.sSearch) !== 0 )
- {
- /* Nuke the old display array - we are going to rebuild it */
- oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);
-
- /* Force a rebuild of the search array */
- _fnBuildSearchArray( oSettings, 1 );
-
- /* Search through all records to populate the search array
- * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1
- * mapping
- */
- for ( i=0 ; i tag - remove it */
- sSearch = sSearch.replace(/\n/g," ").replace(/\r/g,"");
- }
-
- return sSearch;
- }
-
- /**
- * Build a regular expression object suitable for searching a table
- * @param {string} sSearch string to search for
- * @param {bool} bRegex treat as a regular expression or not
- * @param {bool} bSmart perform smart filtering or not
- * @param {bool} bCaseInsensitive Do case insenstive matching or not
- * @returns {RegExp} constructed object
- * @memberof DataTable#oApi
- */
- function _fnFilterCreateSearch( sSearch, bRegex, bSmart, bCaseInsensitive )
- {
- var asSearch, sRegExpString;
-
- if ( bSmart )
- {
- /* Generate the regular expression to use. Something along the lines of:
- * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$
- */
- asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' );
- sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$';
- return new RegExp( sRegExpString, bCaseInsensitive ? "i" : "" );
- }
- else
- {
- sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );
- return new RegExp( sSearch, bCaseInsensitive ? "i" : "" );
- }
- }
-
-
- /**
- * Convert raw data into something that the user can search on
- * @param {string} sData data to be modified
- * @param {string} sType data type
- * @returns {string} search string
- * @memberof DataTable#oApi
- */
- function _fnDataToSearch ( sData, sType )
- {
- if ( typeof DataTable.ext.ofnSearch[sType] === "function" )
- {
- return DataTable.ext.ofnSearch[sType]( sData );
- }
- else if ( sData === null )
- {
- return '';
- }
- else if ( sType == "html" )
- {
- return sData.replace(/[\r\n]/g," ").replace( /<.*?>/g, "" );
- }
- else if ( typeof sData === "string" )
- {
- return sData.replace(/[\r\n]/g," ");
- }
- return sData;
- }
-
-
- /**
- * scape a string stuch that it can be used in a regular expression
- * @param {string} sVal string to escape
- * @returns {string} escaped string
- * @memberof DataTable#oApi
- */
- function _fnEscapeRegex ( sVal )
- {
- var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^' ];
- var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );
- return sVal.replace(reReplace, '\\$1');
- }
-
-
-
- /**
- * Generate the node required for the info display
- * @param {object} oSettings dataTables settings object
- * @returns {node} Information element
- * @memberof DataTable#oApi
- */
- function _fnFeatureHtmlInfo ( oSettings )
- {
- var nInfo = document.createElement( 'div' );
- nInfo.className = oSettings.oClasses.sInfo;
-
- /* Actions that are to be taken once only for this feature */
- if ( !oSettings.aanFeatures.i )
- {
- /* Add draw callback */
- oSettings.aoDrawCallback.push( {
- "fn": _fnUpdateInfo,
- "sName": "information"
- } );
-
- /* Add id */
- nInfo.id = oSettings.sTableId+'_info';
- }
- oSettings.nTable.setAttribute( 'aria-describedby', oSettings.sTableId+'_info' );
-
- return nInfo;
- }
-
-
- /**
- * Update the information elements in the display
- * @param {object} oSettings dataTables settings object
- * @memberof DataTable#oApi
- */
- function _fnUpdateInfo ( oSettings )
- {
- /* Show information about the table */
- if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )
- {
- return;
- }
-
- var
- oLang = oSettings.oLanguage,
- iStart = oSettings._iDisplayStart+1,
- iEnd = oSettings.fnDisplayEnd(),
- iMax = oSettings.fnRecordsTotal(),
- iTotal = oSettings.fnRecordsDisplay(),
- sOut;
-
- if ( iTotal === 0 && iTotal == iMax )
- {
- /* Empty record set */
- sOut = oLang.sInfoEmpty;
- }
- else if ( iTotal === 0 )
- {
- /* Empty record set after filtering */
- sOut = oLang.sInfoEmpty +' '+ oLang.sInfoFiltered;
- }
- else if ( iTotal == iMax )
- {
- /* Normal record set */
- sOut = oLang.sInfo;
- }
- else
- {
- /* Record set after filtering */
- sOut = oLang.sInfo +' '+ oLang.sInfoFiltered;
- }
-
- // Convert the macros
- sOut += oLang.sInfoPostFix;
- sOut = _fnInfoMacros( oSettings, sOut );
-
- if ( oLang.fnInfoCallback !== null )
- {
- sOut = oLang.fnInfoCallback.call( oSettings.oInstance,
- oSettings, iStart, iEnd, iMax, iTotal, sOut );
- }
-
- var n = oSettings.aanFeatures.i;
- for ( var i=0, iLen=n.length ; i';
- var i, iLen;
- var aLengthMenu = oSettings.aLengthMenu;
-
- if ( aLengthMenu.length == 2 && typeof aLengthMenu[0] === 'object' &&
- typeof aLengthMenu[1] === 'object' )
- {
- for ( i=0, iLen=aLengthMenu[0].length ; i'+aLengthMenu[1][i]+'';
- }
- }
- else
- {
- for ( i=0, iLen=aLengthMenu.length ; i'+aLengthMenu[i]+'';
- }
- }
- sStdMenu += '';
-
- var nLength = document.createElement( 'div' );
- if ( !oSettings.aanFeatures.l )
- {
- nLength.id = oSettings.sTableId+'_length';
- }
- nLength.className = oSettings.oClasses.sLength;
- nLength.innerHTML = '';
-
- /*
- * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,
- * and Stefan Skopnik for fixing the fix!
- */
- $('select option[value="'+oSettings._iDisplayLength+'"]', nLength).attr("selected", true);
-
- $('select', nLength).bind( 'change.DT', function(e) {
- var iVal = $(this).val();
-
- /* Update all other length options for the new display */
- var n = oSettings.aanFeatures.l;
- for ( i=0, iLen=n.length ; i oSettings.aiDisplay.length ||
- oSettings._iDisplayLength == -1 )
- {
- oSettings._iDisplayEnd = oSettings.aiDisplay.length;
- }
- else
- {
- oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;
- }
- }
- }
-
-
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Note that most of the paging logic is done in
- * DataTable.ext.oPagination
- */
-
- /**
- * Generate the node required for default pagination
- * @param {object} oSettings dataTables settings object
- * @returns {node} Pagination feature node
- * @memberof DataTable#oApi
- */
- function _fnFeatureHtmlPaginate ( oSettings )
- {
- if ( oSettings.oScroll.bInfinite )
- {
- return null;
- }
-
- var nPaginate = document.createElement( 'div' );
- nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;
-
- DataTable.ext.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate,
- function( oSettings ) {
- _fnCalculateEnd( oSettings );
- _fnDraw( oSettings );
- }
- );
-
- /* Add a draw callback for the pagination on first instance, to update the paging display */
- if ( !oSettings.aanFeatures.p )
- {
- oSettings.aoDrawCallback.push( {
- "fn": function( oSettings ) {
- DataTable.ext.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {
- _fnCalculateEnd( oSettings );
- _fnDraw( oSettings );
- } );
- },
- "sName": "pagination"
- } );
- }
- return nPaginate;
- }
-
-
- /**
- * Alter the display settings to change the page
- * @param {object} oSettings dataTables settings object
- * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last"
- * or page number to jump to (integer)
- * @returns {bool} true page has changed, false - no change (no effect) eg 'first' on page 1
- * @memberof DataTable#oApi
- */
- function _fnPageChange ( oSettings, mAction )
- {
- var iOldStart = oSettings._iDisplayStart;
-
- if ( typeof mAction === "number" )
- {
- oSettings._iDisplayStart = mAction * oSettings._iDisplayLength;
- if ( oSettings._iDisplayStart > oSettings.fnRecordsDisplay() )
- {
- oSettings._iDisplayStart = 0;
- }
- }
- else if ( mAction == "first" )
- {
- oSettings._iDisplayStart = 0;
- }
- else if ( mAction == "previous" )
- {
- oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?
- oSettings._iDisplayStart - oSettings._iDisplayLength :
- 0;
-
- /* Correct for underrun */
- if ( oSettings._iDisplayStart < 0 )
- {
- oSettings._iDisplayStart = 0;
- }
- }
- else if ( mAction == "next" )
- {
- if ( oSettings._iDisplayLength >= 0 )
- {
- /* Make sure we are not over running the display array */
- if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() )
- {
- oSettings._iDisplayStart += oSettings._iDisplayLength;
- }
- }
- else
- {
- oSettings._iDisplayStart = 0;
- }
- }
- else if ( mAction == "last" )
- {
- if ( oSettings._iDisplayLength >= 0 )
- {
- var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1;
- oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength;
- }
- else
- {
- oSettings._iDisplayStart = 0;
- }
- }
- else
- {
- _fnLog( oSettings, 0, "Unknown paging action: "+mAction );
- }
- $(oSettings.oInstance).trigger('page', oSettings);
-
- return iOldStart != oSettings._iDisplayStart;
- }
-
-
-
- /**
- * Generate the node required for the processing node
- * @param {object} oSettings dataTables settings object
- * @returns {node} Processing element
- * @memberof DataTable#oApi
- */
- function _fnFeatureHtmlProcessing ( oSettings )
- {
- var nProcessing = document.createElement( 'div' );
-
- if ( !oSettings.aanFeatures.r )
- {
- nProcessing.id = oSettings.sTableId+'_processing';
- }
- nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
- nProcessing.className = oSettings.oClasses.sProcessing;
- oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );
-
- return nProcessing;
- }
-
-
- /**
- * Display or hide the processing indicator
- * @param {object} oSettings dataTables settings object
- * @param {bool} bShow Show the processing indicator (true) or not (false)
- * @memberof DataTable#oApi
- */
- function _fnProcessingDisplay ( oSettings, bShow )
- {
- if ( oSettings.oFeatures.bProcessing )
- {
- var an = oSettings.aanFeatures.r;
- for ( var i=0, iLen=an.length ; i 0 )
- {
- nCaption = nCaption[0];
- if ( nCaption._captionSide === "top" )
- {
- nScrollHeadTable.appendChild( nCaption );
- }
- else if ( nCaption._captionSide === "bottom" && nTfoot )
- {
- nScrollFootTable.appendChild( nCaption );
- }
- }
-
- /*
- * Sizing
- */
- /* When xscrolling add the width and a scroller to move the header with the body */
- if ( oSettings.oScroll.sX !== "" )
- {
- nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX );
- nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX );
-
- if ( nTfoot !== null )
- {
- nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX );
- }
-
- /* When the body is scrolled, then we also want to scroll the headers */
- $(nScrollBody).scroll( function (e) {
- nScrollHead.scrollLeft = this.scrollLeft;
-
- if ( nTfoot !== null )
- {
- nScrollFoot.scrollLeft = this.scrollLeft;
- }
- } );
- }
-
- /* When yscrolling, add the height */
- if ( oSettings.oScroll.sY !== "" )
- {
- nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY );
- }
-
- /* Redraw - align columns across the tables */
- oSettings.aoDrawCallback.push( {
- "fn": _fnScrollDraw,
- "sName": "scrolling"
- } );
-
- /* Infinite scrolling event handlers */
- if ( oSettings.oScroll.bInfinite )
- {
- $(nScrollBody).scroll( function() {
- /* Use a blocker to stop scrolling from loading more data while other data is still loading */
- if ( !oSettings.bDrawing && $(this).scrollTop() !== 0 )
- {
- /* Check if we should load the next data set */
- if ( $(this).scrollTop() + $(this).height() >
- $(oSettings.nTable).height() - oSettings.oScroll.iLoadGap )
- {
- /* Only do the redraw if we have to - we might be at the end of the data */
- if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() )
- {
- _fnPageChange( oSettings, 'next' );
- _fnCalculateEnd( oSettings );
- _fnDraw( oSettings );
- }
- }
- }
- } );
- }
-
- oSettings.nScrollHead = nScrollHead;
- oSettings.nScrollFoot = nScrollFoot;
-
- return nScroller;
- }
-
-
- /**
- * Update the various tables for resizing. It's a bit of a pig this function, but
- * basically the idea to:
- * 1. Re-create the table inside the scrolling div
- * 2. Take live measurements from the DOM
- * 3. Apply the measurements
- * 4. Clean up
- * @param {object} o dataTables settings object
- * @returns {node} Node to add to the DOM
- * @memberof DataTable#oApi
- */
- function _fnScrollDraw ( o )
- {
- var
- nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],
- nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
- nScrollBody = o.nTable.parentNode,
- i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,
- nTheadSize, nTfootSize,
- iWidth, aApplied=[], iSanityWidth,
- nScrollFootInner = (o.nTFoot !== null) ? o.nScrollFoot.getElementsByTagName('div')[0] : null,
- nScrollFootTable = (o.nTFoot !== null) ? nScrollFootInner.getElementsByTagName('table')[0] : null,
- ie67 = $.browser.msie && $.browser.version <= 7;
-
- /*
- * 1. Re-create the table inside the scrolling div
- */
-
- /* Remove the old minimised thead and tfoot elements in the inner table */
- $(o.nTable).children('thead, tfoot').remove();
-
- /* Clone the current header and footer elements and then place it into the inner table */
- nTheadSize = $(o.nTHead).clone()[0];
- o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );
-
- if ( o.nTFoot !== null )
- {
- nTfootSize = $(o.nTFoot).clone()[0];
- o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );
- }
-
- /*
- * 2. Take live measurements from the DOM - do not alter the DOM itself!
- */
-
- /* Remove old sizing and apply the calculated column widths
- * Get the unique column headers in the newly created (cloned) header. We want to apply the
- * calclated sizes to this header
- */
- if ( o.oScroll.sX === "" )
- {
- nScrollBody.style.width = '100%';
- nScrollHeadInner.parentNode.style.width = '100%';
- }
-
- var nThs = _fnGetUniqueThs( o, nTheadSize );
- for ( i=0, iLen=nThs.length ; i nScrollBody.offsetHeight ||
- $(nScrollBody).css('overflow-y') == "scroll") )
- {
- o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth() - o.oScroll.iBarWidth);
- }
- }
- else
- {
- if ( o.oScroll.sXInner !== "" )
- {
- /* x scroll inner has been given - use it */
- o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);
- }
- else if ( iSanityWidth == $(nScrollBody).width() &&
- $(nScrollBody).height() < $(o.nTable).height() )
- {
- /* There is y-scrolling - try to take account of the y scroll bar */
- o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth );
- if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth )
- {
- /* Not possible to take account of it */
- o.nTable.style.width = _fnStringToCss( iSanityWidth );
- }
- }
- else
- {
- /* All else fails */
- o.nTable.style.width = _fnStringToCss( iSanityWidth );
- }
- }
-
- /* Recalculate the sanity width - now that we've applied the required width, before it was
- * a temporary variable. This is required because the column width calculation is done
- * before this table DOM is created.
- */
- iSanityWidth = $(o.nTable).outerWidth();
-
- /* We want the hidden header to have zero height, so remove padding and borders. Then
- * set the width based on the real headers
- */
- anHeadToSize = o.nTHead.getElementsByTagName('tr');
- anHeadSizers = nTheadSize.getElementsByTagName('tr');
-
- _fnApplyToChildren( function(nSizer, nToSize) {
- oStyle = nSizer.style;
- oStyle.paddingTop = "0";
- oStyle.paddingBottom = "0";
- oStyle.borderTopWidth = "0";
- oStyle.borderBottomWidth = "0";
- oStyle.height = 0;
-
- iWidth = $(nSizer).width();
- nToSize.style.width = _fnStringToCss( iWidth );
- aApplied.push( iWidth );
- }, anHeadSizers, anHeadToSize );
- $(anHeadSizers).height(0);
-
- if ( o.nTFoot !== null )
- {
- /* Clone the current footer and then place it into the body table as a "hidden header" */
- anFootSizers = nTfootSize.getElementsByTagName('tr');
- anFootToSize = o.nTFoot.getElementsByTagName('tr');
-
- _fnApplyToChildren( function(nSizer, nToSize) {
- oStyle = nSizer.style;
- oStyle.paddingTop = "0";
- oStyle.paddingBottom = "0";
- oStyle.borderTopWidth = "0";
- oStyle.borderBottomWidth = "0";
- oStyle.height = 0;
-
- iWidth = $(nSizer).width();
- nToSize.style.width = _fnStringToCss( iWidth );
- aApplied.push( iWidth );
- }, anFootSizers, anFootToSize );
- $(anFootSizers).height(0);
- }
-
- /*
- * 3. Apply the measurements
- */
-
- /* "Hide" the header and footer that we used for the sizing. We want to also fix their width
- * to what they currently are
- */
- _fnApplyToChildren( function(nSizer) {
- nSizer.innerHTML = "";
- nSizer.style.width = _fnStringToCss( aApplied.shift() );
- }, anHeadSizers );
-
- if ( o.nTFoot !== null )
- {
- _fnApplyToChildren( function(nSizer) {
- nSizer.innerHTML = "";
- nSizer.style.width = _fnStringToCss( aApplied.shift() );
- }, anFootSizers );
- }
-
- /* Sanity check that the table is of a sensible width. If not then we are going to get
- * misalignment - try to prevent this by not allowing the table to shrink below its min width
- */
- if ( $(o.nTable).outerWidth() < iSanityWidth )
- {
- /* The min width depends upon if we have a vertical scrollbar visible or not */
- var iCorrection = ((nScrollBody.scrollHeight > nScrollBody.offsetHeight ||
- $(nScrollBody).css('overflow-y') == "scroll")) ?
- iSanityWidth+o.oScroll.iBarWidth : iSanityWidth;
-
- /* IE6/7 are a law unto themselves... */
- if ( ie67 && (nScrollBody.scrollHeight >
- nScrollBody.offsetHeight || $(nScrollBody).css('overflow-y') == "scroll") )
- {
- o.nTable.style.width = _fnStringToCss( iCorrection-o.oScroll.iBarWidth );
- }
-
- /* Apply the calculated minimum width to the table wrappers */
- nScrollBody.style.width = _fnStringToCss( iCorrection );
- nScrollHeadInner.parentNode.style.width = _fnStringToCss( iCorrection );
-
- if ( o.nTFoot !== null )
- {
- nScrollFootInner.parentNode.style.width = _fnStringToCss( iCorrection );
- }
-
- /* And give the user a warning that we've stopped the table getting too small */
- if ( o.oScroll.sX === "" )
- {
- _fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
- " misalignment. The table has been drawn at its minimum possible width." );
- }
- else if ( o.oScroll.sXInner !== "" )
- {
- _fnLog( o, 1, "The table cannot fit into the current element which will cause column"+
- " misalignment. Increase the sScrollXInner value or remove it to allow automatic"+
- " calculation" );
- }
- }
- else
- {
- nScrollBody.style.width = _fnStringToCss( '100%' );
- nScrollHeadInner.parentNode.style.width = _fnStringToCss( '100%' );
-
- if ( o.nTFoot !== null )
- {
- nScrollFootInner.parentNode.style.width = _fnStringToCss( '100%' );
- }
- }
-
-
- /*
- * 4. Clean up
- */
- if ( o.oScroll.sY === "" )
- {
- /* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
- * the scrollbar height from the visible display, rather than adding it on. We need to
- * set the height in order to sort this. Don't want to do it in any other browsers.
- */
- if ( ie67 )
- {
- nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth );
- }
- }
-
- if ( o.oScroll.sY !== "" && o.oScroll.bCollapse )
- {
- nScrollBody.style.height = _fnStringToCss( o.oScroll.sY );
-
- var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?
- o.oScroll.iBarWidth : 0;
- if ( o.nTable.offsetHeight < nScrollBody.offsetHeight )
- {
- nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+iExtra );
- }
- }
-
- /* Finally set the width's of the header and footer tables */
- var iOuterWidth = $(o.nTable).outerWidth();
- nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth );
- nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth );
-
- // Figure out if there are scrollbar present - if so then we need a the header and footer to
- // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar)
- var bScrolling = $(o.nTable).height() > nScrollBody.clientHeight || $(nScrollBody).css('overflow-y') == "scroll";
- nScrollHeadInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
-
- if ( o.nTFoot !== null )
- {
- nScrollFootTable.style.width = _fnStringToCss( iOuterWidth );
- nScrollFootInner.style.width = _fnStringToCss( iOuterWidth );
- nScrollFootInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px";
- }
-
- /* Adjust the position of the header incase we loose the y-scrollbar */
- $(nScrollBody).scroll();
-
- /* If sorting or filtering has occurred, jump the scrolling back to the top */
- if ( o.bSorted || o.bFiltered )
- {
- nScrollBody.scrollTop = 0;
- }
- }
-
-
- /**
- * Apply a given function to the display child nodes of an element array (typically
- * TD children of TR rows
- * @param {function} fn Method to apply to the objects
- * @param array {nodes} an1 List of elements to look through for display children
- * @param array {nodes} an2 Another list (identical structure to the first) - optional
- * @memberof DataTable#oApi
- */
- function _fnApplyToChildren( fn, an1, an2 )
- {
- for ( var i=0, iLen=an1.length ; itd', nCalcTmp);
- }
-
- /* Apply custom sizing to the cloned header */
- var nThs = _fnGetUniqueThs( oSettings, nTheadClone );
- iCorrector = 0;
- for ( i=0 ; i 0 )
- {
- oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );
- }
- iCorrector++;
- }
- }
-
- var cssWidth = $(nCalcTmp).css('width');
- oSettings.nTable.style.width = (cssWidth.indexOf('%') !== -1) ?
- cssWidth : _fnStringToCss( $(nCalcTmp).outerWidth() );
- nCalcTmp.parentNode.removeChild( nCalcTmp );
- }
-
- if ( widthAttr )
- {
- oSettings.nTable.style.width = _fnStringToCss( widthAttr );
- }
- }
-
-
- /**
- * Adjust a table's width to take account of scrolling
- * @param {object} oSettings dataTables settings object
- * @param {node} n table node
- * @memberof DataTable#oApi
- */
- function _fnScrollingWidthAdjust ( oSettings, n )
- {
- if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" )
- {
- /* When y-scrolling only, we want to remove the width of the scroll bar so the table
- * + scroll bar will fit into the area avaialble.
- */
- var iOrigWidth = $(n).width();
- n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth );
- }
- else if ( oSettings.oScroll.sX !== "" )
- {
- /* When x-scrolling both ways, fix the table at it's current size, without adjusting */
- n.style.width = _fnStringToCss( $(n).outerWidth() );
- }
- }
-
-
- /**
- * Get the widest node
- * @param {object} oSettings dataTables settings object
- * @param {int} iCol column of interest
- * @returns {string} max strlens for each column
- * @memberof DataTable#oApi
- */
- function _fnGetWidestNode( oSettings, iCol )
- {
- var iMaxIndex = _fnGetMaxLenString( oSettings, iCol );
- if ( iMaxIndex < 0 )
- {
- return null;
- }
-
- if ( oSettings.aoData[iMaxIndex].nTr === null )
- {
- var n = document.createElement('td');
- n.innerHTML = _fnGetCellData( oSettings, iMaxIndex, iCol, '' );
- return n;
- }
- return _fnGetTdNodes(oSettings, iMaxIndex)[iCol];
- }
-
-
- /**
- * Get the maximum strlen for each data column
- * @param {object} oSettings dataTables settings object
- * @param {int} iCol column of interest
- * @returns {string} max strlens for each column
- * @memberof DataTable#oApi
- */
- function _fnGetMaxLenString( oSettings, iCol )
- {
- var iMax = -1;
- var iMaxIndex = -1;
-
- for ( var i=0 ; i/g, "" );
- if ( s.length > iMax )
- {
- iMax = s.length;
- iMaxIndex = i;
- }
- }
-
- return iMaxIndex;
- }
-
-
- /**
- * Append a CSS unit (only if required) to a string
- * @param {array} aArray1 first array
- * @param {array} aArray2 second array
- * @returns {int} 0 if match, 1 if length is different, 2 if no match
- * @memberof DataTable#oApi
- */
- function _fnStringToCss( s )
- {
- if ( s === null )
- {
- return "0px";
- }
-
- if ( typeof s == 'number' )
- {
- if ( s < 0 )
- {
- return "0px";
- }
- return s+"px";
- }
-
- /* Check if the last character is not 0-9 */
- var c = s.charCodeAt( s.length-1 );
- if (c < 0x30 || c > 0x39)
- {
- return s;
- }
- return s+"px";
- }
-
-
- /**
- * Get the width of a scroll bar in this browser being used
- * @returns {int} width in pixels
- * @memberof DataTable#oApi
- */
- function _fnScrollBarWidth ()
- {
- var inner = document.createElement('p');
- var style = inner.style;
- style.width = "100%";
- style.height = "200px";
- style.padding = "0px";
-
- var outer = document.createElement('div');
- style = outer.style;
- style.position = "absolute";
- style.top = "0px";
- style.left = "0px";
- style.visibility = "hidden";
- style.width = "200px";
- style.height = "150px";
- style.padding = "0px";
- style.overflow = "hidden";
- outer.appendChild(inner);
-
- document.body.appendChild(outer);
- var w1 = inner.offsetWidth;
- outer.style.overflow = 'scroll';
- var w2 = inner.offsetWidth;
- if ( w1 == w2 )
- {
- w2 = outer.clientWidth;
- }
-
- document.body.removeChild(outer);
- return (w1 - w2);
- }
-
-
-
- /**
- * Change the order of the table
- * @param {object} oSettings dataTables settings object
- * @param {bool} bApplyClasses optional - should we apply classes or not
- * @memberof DataTable#oApi
- */
- function _fnSort ( oSettings, bApplyClasses )
- {
- var
- i, iLen, j, jLen, k, kLen,
- sDataType, nTh,
- aaSort = [],
- aiOrig = [],
- oSort = DataTable.ext.oSort,
- aoData = oSettings.aoData,
- aoColumns = oSettings.aoColumns,
- oAria = oSettings.oLanguage.oAria;
-
- /* No sorting required if server-side or no sorting array */
- if ( !oSettings.oFeatures.bServerSide &&
- (oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )
- {
- aaSort = ( oSettings.aaSortingFixed !== null ) ?
- oSettings.aaSortingFixed.concat( oSettings.aaSorting ) :
- oSettings.aaSorting.slice();
-
- /* If there is a sorting data type, and a fuction belonging to it, then we need to
- * get the data from the developer's function and apply it for this column
- */
- for ( i=0 ; i/g, "" );
- nTh = aoColumns[i].nTh;
- nTh.removeAttribute('aria-sort');
- nTh.removeAttribute('aria-label');
-
- /* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */
- if ( aoColumns[i].bSortable )
- {
- if ( aaSort.length > 0 && aaSort[0][0] == i )
- {
- nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" );
-
- var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ?
- aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0];
- nTh.setAttribute('aria-label', sTitle+
- (nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
- }
- else
- {
- nTh.setAttribute('aria-label', sTitle+
- (aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) );
- }
- }
- else
- {
- nTh.setAttribute('aria-label', sTitle);
- }
- }
-
- /* Tell the draw function that we have sorted the data */
- oSettings.bSorted = true;
- $(oSettings.oInstance).trigger('sort', oSettings);
-
- /* Copy the master data into the draw array and re-draw */
- if ( oSettings.oFeatures.bFilter )
- {
- /* _fnFilter() will redraw the table for us */
- _fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );
- }
- else
- {
- oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
- oSettings._iDisplayStart = 0; /* reset display back to page 0 */
- _fnCalculateEnd( oSettings );
- _fnDraw( oSettings );
- }
- }
-
-
- /**
- * Attach a sort handler (click) to a node
- * @param {object} oSettings dataTables settings object
- * @param {node} nNode node to attach the handler to
- * @param {int} iDataIndex column sorting index
- * @param {function} [fnCallback] callback function
- * @memberof DataTable#oApi
- */
- function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )
- {
- _fnBindAction( nNode, {}, function (e) {
- /* If the column is not sortable - don't to anything */
- if ( oSettings.aoColumns[iDataIndex].bSortable === false )
- {
- return;
- }
-
- /*
- * This is a little bit odd I admit... I declare a temporary function inside the scope of
- * _fnBuildHead and the click handler in order that the code presented here can be used
- * twice - once for when bProcessing is enabled, and another time for when it is
- * disabled, as we need to perform slightly different actions.
- * Basically the issue here is that the Javascript engine in modern browsers don't
- * appear to allow the rendering engine to update the display while it is still excuting
- * it's thread (well - it does but only after long intervals). This means that the
- * 'processing' display doesn't appear for a table sort. To break the js thread up a bit
- * I force an execution break by using setTimeout - but this breaks the expected
- * thread continuation for the end-developer's point of view (their code would execute
- * too early), so we on;y do it when we absolutely have to.
- */
- var fnInnerSorting = function () {
- var iColumn, iNextSort;
-
- /* If the shift key is pressed then we are multipe column sorting */
- if ( e.shiftKey )
- {
- /* Are we already doing some kind of sort on this column? */
- var bFound = false;
- for ( var i=0 ; i= iColumns )
- {
- for ( i=0 ; i 4096 ) /* Magic 10 for padding */
- {
- var aCookies =document.cookie.split(';');
- for ( var i=0, iLen=aCookies.length ; i=0 ; i-- )
- {
- aRet.push( aoStore[i].fn.apply( oSettings.oInstance, aArgs ) );
- }
-
- if ( sTrigger !== null )
- {
- $(oSettings.oInstance).trigger(sTrigger, aArgs);
- }
-
- return aRet;
- }
-
-
- /**
- * JSON stringify. If JSON.stringify it provided by the browser, json2.js or any other
- * library, then we use that as it is fast, safe and accurate. If the function isn't
- * available then we need to built it ourselves - the insperation for this function comes
- * from Craig Buckler ( http://www.sitepoint.com/javascript-json-serialization/ ). It is
- * not perfect and absolutely should not be used as a replacement to json2.js - but it does
- * do what we need, without requiring a dependency for DataTables.
- * @param {object} o JSON object to be converted
- * @returns {string} JSON string
- * @memberof DataTable#oApi
- */
- var _fnJsonString = (window.JSON) ? JSON.stringify : function( o )
- {
- /* Not an object or array */
- var sType = typeof o;
- if (sType !== "object" || o === null)
- {
- // simple data type
- if (sType === "string")
- {
- o = '"'+o+'"';
- }
- return o+"";
- }
-
- /* If object or array, need to recurse over it */
- var
- sProp, mValue,
- json = [],
- bArr = $.isArray(o);
-
- for (sProp in o)
- {
- mValue = o[sProp];
- sType = typeof mValue;
-
- if (sType === "string")
- {
- mValue = '"'+mValue+'"';
- }
- else if (sType === "object" && mValue !== null)
- {
- mValue = _fnJsonString(mValue);
- }
-
- json.push((bArr ? "" : '"'+sProp+'":') + mValue);
- }
-
- return (bArr ? "[" : "{") + json + (bArr ? "]" : "}");
- };
-
-
-
-
- /**
- * Perform a jQuery selector action on the table's TR elements (from the tbody) and
- * return the resulting jQuery object.
- * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on
- * @param {object} [oOpts] Optional parameters for modifying the rows to be included
- * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter
- * criterion ("applied") or all TR elements (i.e. no filter).
- * @param {string} [oOpts.order=current] Order of the TR elements in the processed array.
- * Can be either 'current', whereby the current sorting of the table is used, or
- * 'original' whereby the original order the data was read into the table is used.
- * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page
- * ("current") or not ("all"). If 'current' is given, then order is assumed to be
- * 'current' and filter is 'applied', regardless of what they might be given as.
- * @returns {object} jQuery object, filtered by the given selector.
- * @dtopt API
- *
- * @example
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable();
- *
- * // Highlight every second row
- * oTable.$('tr:odd').css('backgroundColor', 'blue');
- * } );
- *
- * @example
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable();
- *
- * // Filter to rows with 'Webkit' in them, add a background colour and then
- * // remove the filter, thus highlighting the 'Webkit' rows only.
- * oTable.fnFilter('Webkit');
- * oTable.$('tr', {"filter": "applied"}).css('backgroundColor', 'blue');
- * oTable.fnFilter('');
- * } );
- */
- this.$ = function ( sSelector, oOpts )
- {
- var i, iLen, a = [];
- var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
-
- if ( !oOpts )
- {
- oOpts = {};
- }
-
- oOpts = $.extend( {}, {
- "filter": "none", // applied
- "order": "current", // "original"
- "page": "all" // current
- }, oOpts );
-
- // Current page implies that order=current and fitler=applied, since it is fairly
- // senseless otherwise
- if ( oOpts.page == 'current' )
- {
- for ( i=oSettings._iDisplayStart, iLen=oSettings.fnDisplayEnd() ; i
- *
1D array of data - add a single row with the data provided
- *
2D array of arrays - add multiple rows in a single call
- *
object - data object when using mDataProp
- *
array of objects - multiple data objects when using mDataProp
- *
- * @param {bool} [bRedraw=true] redraw the table or not
- * @returns {array} An array of integers, representing the list of indexes in
- * aoData ({@link DataTable.models.oSettings}) that have been added to
- * the table.
- * @dtopt API
- *
- * @example
- * // Global var for counter
- * var giCount = 2;
- *
- * $(document).ready(function() {
- * $('#example').dataTable();
- * } );
- *
- * function fnClickAddRow() {
- * $('#example').dataTable().fnAddData( [
- * giCount+".1",
- * giCount+".2",
- * giCount+".3",
- * giCount+".4" ]
- * );
- *
- * giCount++;
- * }
- */
- this.fnAddData = function( mData, bRedraw )
- {
- if ( mData.length === 0 )
- {
- return [];
- }
-
- var aiReturn = [];
- var iTest;
-
- /* Find settings from table node */
- var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
-
- /* Check if we want to add multiple rows or not */
- if ( typeof mData[0] === "object" && mData[0] !== null )
- {
- for ( var i=0 ; i= oSettings.aiDisplay.length )
- {
- oSettings._iDisplayStart -= oSettings._iDisplayLength;
- if ( oSettings._iDisplayStart < 0 )
- {
- oSettings._iDisplayStart = 0;
- }
- }
-
- if ( bRedraw === undefined || bRedraw )
- {
- _fnCalculateEnd( oSettings );
- _fnDraw( oSettings );
- }
-
- return oData;
- };
-
-
- /**
- * Restore the table to it's original state in the DOM by removing all of DataTables
- * enhancements, alterations to the DOM structure of the table and event listeners.
- * @param {boolean} [bRemove=false] Completely remove the table from the DOM
- * @dtopt API
- *
- * @example
- * $(document).ready(function() {
- * // This example is fairly pointless in reality, but shows how fnDestroy can be used
- * var oTable = $('#example').dataTable();
- * oTable.fnDestroy();
- * } );
- */
- this.fnDestroy = function ( bRemove )
- {
- var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] );
- var nOrig = oSettings.nTableWrapper.parentNode;
- var nBody = oSettings.nTBody;
- var i, iLen;
-
- bRemove = (bRemove===undefined) ? false : true;
-
- /* Flag to note that the table is currently being destroyed - no action should be taken */
- oSettings.bDestroying = true;
-
- /* Fire off the destroy callbacks for plug-ins etc */
- _fnCallbackFire( oSettings, "aoDestroyCallback", "destroy", [oSettings] );
-
- /* Restore hidden columns */
- for ( i=0, iLen=oSettings.aoColumns.length ; itr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();
-
- /* When scrolling we had to break the table up - restore it */
- if ( oSettings.nTable != oSettings.nTHead.parentNode )
- {
- $(oSettings.nTable).children('thead').remove();
- oSettings.nTable.appendChild( oSettings.nTHead );
- }
-
- if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )
- {
- $(oSettings.nTable).children('tfoot').remove();
- oSettings.nTable.appendChild( oSettings.nTFoot );
- }
-
- /* Remove the DataTables generated nodes, events and classes */
- oSettings.nTable.parentNode.removeChild( oSettings.nTable );
- $(oSettings.nTableWrapper).remove();
-
- oSettings.aaSorting = [];
- oSettings.aaSortingFixed = [];
- _fnSortingClasses( oSettings );
-
- $(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripeClasses.join(' ') );
-
- $('th, td', oSettings.nTHead).removeClass( [
- oSettings.oClasses.sSortable,
- oSettings.oClasses.sSortableAsc,
- oSettings.oClasses.sSortableDesc,
- oSettings.oClasses.sSortableNone ].join(' ')
- );
- if ( oSettings.bJUI )
- {
- $('th span.'+oSettings.oClasses.sSortIcon
- + ', td span.'+oSettings.oClasses.sSortIcon, oSettings.nTHead).remove();
-
- $('th, td', oSettings.nTHead).each( function () {
- var jqWrapper = $('div.'+oSettings.oClasses.sSortJUIWrapper, this);
- var kids = jqWrapper.contents();
- $(this).append( kids );
- jqWrapper.remove();
- } );
- }
-
- /* Add the TR elements back into the table in their original order */
- if ( !bRemove && oSettings.nTableReinsertBefore )
- {
- nOrig.insertBefore( oSettings.nTable, oSettings.nTableReinsertBefore );
- }
- else if ( !bRemove )
- {
- nOrig.appendChild( oSettings.nTable );
- }
-
- for ( i=0, iLen=oSettings.aoData.length ; i= _fnVisbleColumns( oSettings ));
-
- /* Which coloumn should we be inserting before? */
- if ( !bAppend )
- {
- for ( i=iCol ; i= oSettings.aoColumns.length )
- {
- oSettings.aaSorting[i][0] = 0;
- }
- var oColumn = oSettings.aoColumns[ oSettings.aaSorting[i][0] ];
-
- /* Add a default sorting index */
- if ( oSettings.aaSorting[i][2] === undefined )
- {
- oSettings.aaSorting[i][2] = 0;
- }
-
- /* If aaSorting is not defined, then we use the first indicator in asSorting */
- if ( oInit.aaSorting === undefined && oSettings.saved_aaSorting === undefined )
- {
- oSettings.aaSorting[i][1] = oColumn.asSorting[0];
- }
-
- /* Set the current sorting index based on aoColumns.asSorting */
- for ( j=0, jLen=oColumn.asSorting.length ; j 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") )
- {
- // If we are a scrolling table, and no footer has been given, then we need to create
- // a tfoot element for the caption element to be appended to
- tfoot = [ document.createElement( 'tfoot' ) ];
- this.appendChild( tfoot[0] );
- }
-
- if ( tfoot.length > 0 )
- {
- oSettings.nTFoot = tfoot[0];
- _fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot );
- }
-
- /* Check if there is data passing into the constructor */
- if ( bUsePassedData )
- {
- for ( i=0 ; i= parseInt(sThat, 10);
- };
-
-
- /**
- * Check if a TABLE node is a DataTable table already or not.
- * @param {node} nTable The TABLE node to check if it is a DataTable or not (note that other
- * node types can be passed in, but will always return false).
- * @returns {boolean} true the table given is a DataTable, or false otherwise
- * @static
- * @dtopt API-Static
- *
- * @example
- * var ex = document.getElementById('example');
- * if ( ! $.fn.DataTable.fnIsDataTable( ex ) ) {
- * $(ex).dataTable();
- * }
- */
- DataTable.fnIsDataTable = function ( nTable )
- {
- var o = DataTable.settings;
-
- for ( var i=0 ; i 0 ) {
- * $(table).dataTable().fnAdjustColumnSizing();
- * }
- */
- DataTable.fnTables = function ( bVisible )
- {
- var out = [];
-
- jQuery.each( DataTable.settings, function (i, o) {
- if ( !bVisible || (bVisible === true && $(o.nTable).is(':visible')) )
- {
- out.push( o.nTable );
- }
- } );
-
- return out;
- };
-
-
- /**
- * Version string for plug-ins to check compatibility. Allowed format is
- * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
- * e are optional
- * @member
- * @type string
- * @default Version number
- */
- DataTable.version = "1.9.1";
-
- /**
- * Private data store, containing all of the settings objects that are created for the
- * tables on a given page.
- *
- * Note that the DataTable.settings object is aliased to jQuery.fn.dataTableExt
- * through which it may be accessed and manipulated, or jQuery.fn.dataTable.settings.
- * @member
- * @type array
- * @default []
- * @private
- */
- DataTable.settings = [];
-
- /**
- * Object models container, for the various models that DataTables has available
- * to it. These models define the objects that are used to hold the active state
- * and configuration of the table.
- * @namespace
- */
- DataTable.models = {};
-
-
- /**
- * DataTables extension options and plug-ins. This namespace acts as a collection "area"
- * for plug-ins that can be used to extend the default DataTables behaviour - indeed many
- * of the build in methods use this method to provide their own capabilities (sorting methods
- * for example).
- *
- * Note that this namespace is aliased to jQuery.fn.dataTableExt so it can be readily accessed
- * and modified by plug-ins.
- * @namespace
- */
- DataTable.models.ext = {
- /**
- * Plug-in filtering functions - this method of filtering is complimentary to the default
- * type based filtering, and a lot more comprehensive as it allows you complete control
- * over the filtering logic. Each element in this array is a function (parameters
- * described below) that is called for every row in the table, and your logic decides if
- * it should be included in the filtered data set or not.
- *
- *
- * Function input parameters:
- *
- *
{object} DataTables settings object: see {@link DataTable.models.oSettings}.
- *
{array|object} Data for the row to be processed (same as the original format
- * that was passed in as the data source, or an array from a DOM data source
- *
{int} Row index in aoData ({@link DataTable.models.oSettings.aoData}), which can
- * be useful to retrieve the TR element if you need DOM interaction.
- *
- *
- *
- * Function return:
- *
- *
{boolean} Include the row in the filtered result set (true) or not (false)
- *
- *
- *
- * @type array
- * @default []
- *
- * @example
- * // The following example shows custom filtering being applied to the fourth column (i.e.
- * // the aData[3] index) based on two input values from the end-user, matching the data in
- * // a certain range.
- * $.fn.dataTableExt.afnFiltering.push(
- * function( oSettings, aData, iDataIndex ) {
- * var iMin = document.getElementById('min').value * 1;
- * var iMax = document.getElementById('max').value * 1;
- * var iVersion = aData[3] == "-" ? 0 : aData[3]*1;
- * if ( iMin == "" && iMax == "" ) {
- * return true;
- * }
- * else if ( iMin == "" && iVersion < iMax ) {
- * return true;
- * }
- * else if ( iMin < iVersion && "" == iMax ) {
- * return true;
- * }
- * else if ( iMin < iVersion && iVersion < iMax ) {
- * return true;
- * }
- * return false;
- * }
- * );
- */
- "afnFiltering": [],
-
-
- /**
- * Plug-in sorting functions - this method of sorting is complimentary to the default type
- * based sorting that DataTables does automatically, allowing much greater control over the
- * the data that is being used to sort a column. This is useful if you want to do sorting
- * based on live data (for example the contents of an 'input' element) rather than just the
- * static string that DataTables knows of. The way these plug-ins work is that you create
- * an array of the values you wish to be sorted for the column in question and then return
- * that array. Which pre-sorting function is run here depends on the sSortDataType parameter
- * that is used for the column (if any). This is the corollary of ofnSearch for sort
- * data.
- *
- *
- * Function input parameters:
- *
- *
{object} DataTables settings object: see {@link DataTable.models.oSettings}.
- *
{int} Target column index
- *
- *
- *
- * Function return:
- *
- *
{array} Data for the column to be sorted upon
- *
- *
- *
- *
- * Note that as of v1.9, it is typically preferable to use mDataProp to prepare data for
- * the different uses that DataTables can put the data to. Specifically mDataProp when
- * used as a function will give you a 'type' (sorting, filtering etc) that you can use to
- * prepare the data as required for the different types. As such, this method is deprecated.
- * @type array
- * @default []
- * @deprecated
- *
- * @example
- * // Updating the cached sorting information with user entered values in HTML input elements
- * jQuery.fn.dataTableExt.afnSortData['dom-text'] = function ( oSettings, iColumn )
- * {
- * var aData = [];
- * $( 'td:eq('+iColumn+') input', oSettings.oApi._fnGetTrNodes(oSettings) ).each( function () {
- * aData.push( this.value );
- * } );
- * return aData;
- * }
- */
- "afnSortData": [],
-
-
- /**
- * Feature plug-ins - This is an array of objects which describe the feature plug-ins that are
- * available to DataTables. These feature plug-ins are accessible through the sDom initialisation
- * option. As such, each feature plug-in must describe a function that is used to initialise
- * itself (fnInit), a character so the feature can be enabled by sDom (cFeature) and the name
- * of the feature (sFeature). Thus the objects attached to this method must provide:
- *
- *
{function} fnInit Initialisation of the plug-in
- *
- *
- * Function input parameters:
- *
- *
{object} DataTables settings object: see {@link DataTable.models.oSettings}.
- *
- *
- *
- * Function return:
- *
- *
{node|null} The element which contains your feature. Note that the return
- * may also be void if your plug-in does not require to inject any DOM elements
- * into DataTables control (sDom) - for example this might be useful when
- * developing a plug-in which allows table control via keyboard entry.
- *
- *
- *
- *
- *
{character} cFeature Character that will be matched in sDom - case sensitive
- *
{string} sFeature Feature name
- *
- * @type array
- * @default []
- *
- * @example
- * // How TableTools initialises itself.
- * $.fn.dataTableExt.aoFeatures.push( {
- * "fnInit": function( oSettings ) {
- * return new TableTools( { "oDTSettings": oSettings } );
- * },
- * "cFeature": "T",
- * "sFeature": "TableTools"
- * } );
- */
- "aoFeatures": [],
-
-
- /**
- * Type detection plug-in functions - DataTables utilises types to define how sorting and
- * filtering behave, and types can be either be defined by the developer (sType for the
- * column) or they can be automatically detected by the methods in this array. The functions
- * defined in the array are quite simple, taking a single parameter (the data to analyse)
- * and returning the type if it is a known type, or null otherwise.
- *
- *
- * Function input parameters:
- *
- *
{*} Data from the column cell to be analysed
- *
- *
- *
- * Function return:
- *
- *
{string|null} Data type detected, or null if unknown (and thus pass it
- * on to the other type detection functions.
- *
- *
- *
- * @type array
- * @default []
- *
- * @example
- * // Currency type detection plug-in:
- * jQuery.fn.dataTableExt.aTypes.push(
- * function ( sData ) {
- * var sValidChars = "0123456789.-";
- * var Char;
- *
- * // Check the numeric part
- * for ( i=1 ; iafnSortData for filtering data.
- *
- *
- * Function input parameters:
- *
- *
{*} Data from the column cell to be prepared for filtering
- *
- *
- *
- * Function return:
- *
- *
{string|null} Formatted string that will be used for the filtering.
- *
- *
- *
- *
- * Note that as of v1.9, it is typically preferable to use mDataProp to prepare data for
- * the different uses that DataTables can put the data to. Specifically mDataProp when
- * used as a function will give you a 'type' (sorting, filtering etc) that you can use to
- * prepare the data as required for the different types. As such, this method is deprecated.
- * @type object
- * @default {}
- * @deprecated
- *
- * @example
- * $.fn.dataTableExt.ofnSearch['title-numeric'] = function ( sData ) {
- * return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );
- * }
- */
- "ofnSearch": {},
-
-
- /**
- * Container for all private functions in DataTables so they can be exposed externally
- * @type object
- * @default {}
- */
- "oApi": {},
-
-
- /**
- * Storage for the various classes that DataTables uses
- * @type object
- * @default {}
- */
- "oStdClasses": {},
-
-
- /**
- * Storage for the various classes that DataTables uses - jQuery UI suitable
- * @type object
- * @default {}
- */
- "oJUIClasses": {},
-
-
- /**
- * Pagination plug-in methods - The style and controls of the pagination can significantly
- * impact on how the end user interacts with the data in your table, and DataTables allows
- * the addition of pagination controls by extending this object, which can then be enabled
- * through the sPaginationType initialisation parameter. Each pagination type that
- * is added is an object (the property name of which is what sPaginationType refers
- * to) that has two properties, both methods that are used by DataTables to update the
- * control's state.
- *
- *
- * fnInit - Initialisation of the paging controls. Called only during initialisation
- * of the table. It is expected that this function will add the required DOM elements
- * to the page for the paging controls to work. The element pointer
- * 'oSettings.aanFeatures.p' array is provided by DataTables to contain the paging
- * controls (note that this is a 2D array to allow for multiple instances of each
- * DataTables DOM element). It is suggested that you add the controls to this element
- * as children
- *
- *
- * Function input parameters:
- *
- *
{object} DataTables settings object: see {@link DataTable.models.oSettings}.
- *
{node} Container into which the pagination controls must be inserted
- *
{function} Draw callback function - whenever the controls cause a page
- * change, this method must be called to redraw the table.
- *
- *
- *
- * Function return:
- *
- *
No return required
- *
- *
- *
- *
- *
- * fnInit - This function is called whenever the paging status of the table changes and is
- * typically used to update classes and/or text of the paging controls to reflex the new
- * status.
- *
- *
- * Function input parameters:
- *
- *
{object} DataTables settings object: see {@link DataTable.models.oSettings}.
- *
{function} Draw callback function - in case you need to redraw the table again
- * or attach new event listeners
{int} Sorting match: <0 if first parameter should be sorted lower than
- * the second parameter, ===0 if the two parameters are equal and >0 if
- * the first parameter should be sorted height than the second parameter.
- *
- *
- *
- * @type object
- * @default {}
- *
- * @example
- * // Case-sensitive string sorting, with no pre-formatting method
- * $.extend( $.fn.dataTableExt.oSort, {
- * "string-case-asc": function(x,y) {
- * return ((x < y) ? -1 : ((x > y) ? 1 : 0));
- * },
- * "string-case-desc": function(x,y) {
- * return ((x < y) ? 1 : ((x > y) ? -1 : 0));
- * }
- * } );
- *
- * @example
- * // Case-insensitive string sorting, with pre-formatting
- * $.extend( $.fn.dataTableExt.oSort, {
- * "string-pre": function(x) {
- * return x.toLowerCase();
- * },
- * "string-asc": function(x,y) {
- * return ((x < y) ? -1 : ((x > y) ? 1 : 0));
- * },
- * "string-desc": function(x,y) {
- * return ((x < y) ? 1 : ((x > y) ? -1 : 0));
- * }
- * } );
- */
- "oSort": {},
-
-
- /**
- * Version string for plug-ins to check compatibility. Allowed format is
- * a.b.c.d.e where: a:int, b:int, c:int, d:string(dev|beta), e:int. d and
- * e are optional
- * @type string
- * @default Version number
- */
- "sVersion": DataTable.version,
-
-
- /**
- * How should DataTables report an error. Can take the value 'alert' or 'throw'
- * @type string
- * @default alert
- */
- "sErrMode": "alert",
-
-
- /**
- * Store information for DataTables to access globally about other instances
- * @namespace
- * @private
- */
- "_oExternConfig": {
- /* int:iNextUnique - next unique number for an instance */
- "iNextUnique": 0
- }
- };
-
-
-
-
- /**
- * Template object for the way in which DataTables holds information about
- * search information for the global filter and individual column filters.
- * @namespace
- */
- DataTable.models.oSearch = {
- /**
- * Flag to indicate if the filtering should be case insensitive or not
- * @type boolean
- * @default true
- */
- "bCaseInsensitive": true,
-
- /**
- * Applied search term
- * @type string
- * @default Empty string
- */
- "sSearch": "",
-
- /**
- * Flag to indicate if the search term should be interpreted as a
- * regular expression (true) or not (false) and therefore and special
- * regex characters escaped.
- * @type boolean
- * @default false
- */
- "bRegex": false,
-
- /**
- * Flag to indicate if DataTables is to use its smart filtering or not.
- * @type boolean
- * @default true
- */
- "bSmart": true
- };
-
-
-
-
- /**
- * Template object for the way in which DataTables holds information about
- * each individual row. This is the object format used for the settings
- * aoData array.
- * @namespace
- */
- DataTable.models.oRow = {
- /**
- * TR element for the row
- * @type node
- * @default null
- */
- "nTr": null,
-
- /**
- * Data object from the original data source for the row. This is either
- * an array if using the traditional form of DataTables, or an object if
- * using mDataProp options. The exact type will depend on the passed in
- * data from the data source, or will be an array if using DOM a data
- * source.
- * @type array|object
- * @default []
- */
- "_aData": [],
-
- /**
- * Sorting data cache - this array is ostensibly the same length as the
- * number of columns (although each index is generated only as it is
- * needed), and holds the data that is used for sorting each column in the
- * row. We do this cache generation at the start of the sort in order that
- * the formatting of the sort data need be done only once for each cell
- * per sort. This array should not be read from or written to by anything
- * other than the master sorting methods.
- * @type array
- * @default []
- * @private
- */
- "_aSortData": [],
-
- /**
- * Array of TD elements that are cached for hidden rows, so they can be
- * reinserted into the table if a column is made visible again (or to act
- * as a store if a column is made hidden). Only hidden columns have a
- * reference in the array. For non-hidden columns the value is either
- * undefined or null.
- * @type array nodes
- * @default []
- * @private
- */
- "_anHidden": [],
-
- /**
- * Cache of the class name that DataTables has applied to the row, so we
- * can quickly look at this variable rather than needing to do a DOM check
- * on className for the nTr property.
- * @type string
- * @default Empty string
- * @private
- */
- "_sRowStripe": ""
- };
-
-
-
- /**
- * Template object for the column information object in DataTables. This object
- * is held in the settings aoColumns array and contains all the information that
- * DataTables needs about each individual column.
- *
- * Note that this object is related to {@link DataTable.defaults.columns}
- * but this one is the internal data store for DataTables's cache of columns.
- * It should NOT be manipulated outside of DataTables. Any configuration should
- * be done through the initialisation options.
- * @namespace
- */
- DataTable.models.oColumn = {
- /**
- * A list of the columns that sorting should occur on when this column
- * is sorted. That this property is an array allows multi-column sorting
- * to be defined for a column (for example first name / last name columns
- * would benefit from this). The values are integers pointing to the
- * columns to be sorted on (typically it will be a single integer pointing
- * at itself, but that doesn't need to be the case).
- * @type array
- */
- "aDataSort": null,
-
- /**
- * Define the sorting directions that are applied to the column, in sequence
- * as the column is repeatedly sorted upon - i.e. the first value is used
- * as the sorting direction when the column if first sorted (clicked on).
- * Sort it again (click again) and it will move on to the next index.
- * Repeat until loop.
- * @type array
- */
- "asSorting": null,
-
- /**
- * Flag to indicate if the column is searchable, and thus should be included
- * in the filtering or not.
- * @type boolean
- */
- "bSearchable": null,
-
- /**
- * Flag to indicate if the column is sortable or not.
- * @type boolean
- */
- "bSortable": null,
-
- /**
- * When using fnRender, you have two options for what to do with the data,
- * and this property serves as the switch. Firstly, you can have the sorting
- * and filtering use the rendered value (true - default), or you can have
- * the sorting and filtering us the original value (false).
- *
- * *NOTE* It is it is advisable now to use mDataProp as a function and make
- * use of the 'type' that it gives, allowing (potentially) different data to
- * be used for sorting, filtering, display and type detection.
- * @type boolean
- * @deprecated
- */
- "bUseRendered": null,
-
- /**
- * Flag to indicate if the column is currently visible in the table or not
- * @type boolean
- */
- "bVisible": null,
-
- /**
- * Flag to indicate to the type detection method if the automatic type
- * detection should be used, or if a column type (sType) has been specified
- * @type boolean
- * @default true
- * @private
- */
- "_bAutoType": true,
-
- /**
- * Developer definable function that is called whenever a cell is created (Ajax source,
- * etc) or processed for input (DOM source). This can be used as a compliment to fnRender
- * allowing you to modify the DOM element (add background colour for example) when the
- * element is available (since it is not when fnRender is called).
- * @type function
- * @param {element} nTd The TD node that has been created
- * @param {*} sData The Data for the cell
- * @param {array|object} oData The data for the whole row
- * @param {int} iRow The row index for the aoData data store
- * @default null
- */
- "fnCreatedCell": null,
-
- /**
- * Function to get data from a cell in a column. You should never
- * access data directly through _aData internally in DataTables - always use
- * the method attached to this property. It allows mDataProp to function as
- * required. This function is automatically assigned by the column
- * initialisation method
- * @type function
- * @param {array|object} oData The data array/object for the array
- * (i.e. aoData[]._aData)
- * @param {string} sSpecific The specific data type you want to get -
- * 'display', 'type' 'filter' 'sort'
- * @returns {*} The data for the cell from the given row's data
- * @default null
- */
- "fnGetData": null,
-
- /**
- * Custom display function that will be called for the display of each cell
- * in this column.
- * @type function
- * @param {object} o Object with the following parameters:
- * @param {int} o.iDataRow The row in aoData
- * @param {int} o.iDataColumn The column in question
- * @param {array o.aData The data for the row in question
- * @param {object} o.oSettings The settings object for this DataTables instance
- * @returns {string} The string you which to use in the display
- * @default null
- */
- "fnRender": null,
-
- /**
- * Function to set data for a cell in the column. You should never
- * set the data directly to _aData internally in DataTables - always use
- * this method. It allows mDataProp to function as required. This function
- * is automatically assigned by the column initialisation method
- * @type function
- * @param {array|object} oData The data array/object for the array
- * (i.e. aoData[]._aData)
- * @param {*} sValue Value to set
- * @default null
- */
- "fnSetData": null,
-
- /**
- * Property to read the value for the cells in the column from the data
- * source array / object. If null, then the default content is used, if a
- * function is given then the return from the function is used.
- * @type function|int|string|null
- * @default null
- */
- "mDataProp": null,
-
- /**
- * Unique header TH/TD element for this column - this is what the sorting
- * listener is attached to (if sorting is enabled.)
- * @type node
- * @default null
- */
- "nTh": null,
-
- /**
- * Unique footer TH/TD element for this column (if there is one). Not used
- * in DataTables as such, but can be used for plug-ins to reference the
- * footer for each column.
- * @type node
- * @default null
- */
- "nTf": null,
-
- /**
- * The class to apply to all TD elements in the table's TBODY for the column
- * @type string
- * @default null
- */
- "sClass": null,
-
- /**
- * When DataTables calculates the column widths to assign to each column,
- * it finds the longest string in each column and then constructs a
- * temporary table and reads the widths from that. The problem with this
- * is that "mmm" is much wider then "iiii", but the latter is a longer
- * string - thus the calculation can go wrong (doing it properly and putting
- * it into an DOM object and measuring that is horribly(!) slow). Thus as
- * a "work around" we provide this option. It will append its value to the
- * text that is found to be the longest string for the column - i.e. padding.
- * @type string
- */
- "sContentPadding": null,
-
- /**
- * Allows a default value to be given for a column's data, and will be used
- * whenever a null data source is encountered (this can be because mDataProp
- * is set to null, or because the data source itself is null).
- * @type string
- * @default null
- */
- "sDefaultContent": null,
-
- /**
- * Name for the column, allowing reference to the column by name as well as
- * by index (needs a lookup to work by name).
- * @type string
- */
- "sName": null,
-
- /**
- * Custom sorting data type - defines which of the available plug-ins in
- * afnSortData the custom sorting will use - if any is defined.
- * @type string
- * @default std
- */
- "sSortDataType": 'std',
-
- /**
- * Class to be applied to the header element when sorting on this column
- * @type string
- * @default null
- */
- "sSortingClass": null,
-
- /**
- * Class to be applied to the header element when sorting on this column -
- * when jQuery UI theming is used.
- * @type string
- * @default null
- */
- "sSortingClassJUI": null,
-
- /**
- * Title of the column - what is seen in the TH element (nTh).
- * @type string
- */
- "sTitle": null,
-
- /**
- * Column sorting and filtering type
- * @type string
- * @default null
- */
- "sType": null,
-
- /**
- * Width of the column
- * @type string
- * @default null
- */
- "sWidth": null,
-
- /**
- * Width of the column when it was first "encountered"
- * @type string
- * @default null
- */
- "sWidthOrig": null
- };
-
-
-
- /**
- * Initialisation options that can be given to DataTables at initialisation
- * time.
- * @namespace
- */
- DataTable.defaults = {
- /**
- * An array of data to use for the table, passed in at initialisation which
- * will be used in preference to any data which is already in the DOM. This is
- * particularly useful for constructing tables purely in Javascript, for
- * example with a custom Ajax call.
- * @type array
- * @default null
- * @dtopt Option
- *
- * @example
- * // Using a 2D array data source
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "aaData": [
- * ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'],
- * ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'],
- * ],
- * "aoColumns": [
- * { "sTitle": "Engine" },
- * { "sTitle": "Browser" },
- * { "sTitle": "Platform" },
- * { "sTitle": "Version" },
- * { "sTitle": "Grade" }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using an array of objects as a data source (mDataProp)
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "aaData": [
- * {
- * "engine": "Trident",
- * "browser": "Internet Explorer 4.0",
- * "platform": "Win 95+",
- * "version": 4,
- * "grade": "X"
- * },
- * {
- * "engine": "Trident",
- * "browser": "Internet Explorer 5.0",
- * "platform": "Win 95+",
- * "version": 5,
- * "grade": "C"
- * }
- * ],
- * "aoColumns": [
- * { "sTitle": "Engine", "mDataProp": "engine" },
- * { "sTitle": "Browser", "mDataProp": "browser" },
- * { "sTitle": "Platform", "mDataProp": "platform" },
- * { "sTitle": "Version", "mDataProp": "version" },
- * { "sTitle": "Grade", "mDataProp": "grade" }
- * ]
- * } );
- * } );
- */
- "aaData": null,
-
-
- /**
- * If sorting is enabled, then DataTables will perform a first pass sort on
- * initialisation. You can define which column(s) the sort is performed upon,
- * and the sorting direction, with this variable. The aaSorting array should
- * contain an array for each column to be sorted initially containing the
- * column's index and a direction string ('asc' or 'desc').
- * @type array
- * @default [[0,'asc']]
- * @dtopt Option
- *
- * @example
- * // Sort by 3rd column first, and then 4th column
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "aaSorting": [[2,'asc'], [3,'desc']]
- * } );
- * } );
- *
- * // No initial sorting
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "aaSorting": []
- * } );
- * } );
- */
- "aaSorting": [[0,'asc']],
-
-
- /**
- * This parameter is basically identical to the aaSorting parameter, but
- * cannot be overridden by user interaction with the table. What this means
- * is that you could have a column (visible or hidden) which the sorting will
- * always be forced on first - any sorting after that (from the user) will
- * then be performed as required. This can be useful for grouping rows
- * together.
- * @type array
- * @default null
- * @dtopt Option
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "aaSortingFixed": [[0,'asc']]
- * } );
- * } )
- */
- "aaSortingFixed": null,
-
-
- /**
- * This parameter allows you to readily specify the entries in the length drop
- * down menu that DataTables shows when pagination is enabled. It can be
- * either a 1D array of options which will be used for both the displayed
- * option and the value, or a 2D array which will use the array in the first
- * position as the value, and the array in the second position as the
- * displayed options (useful for language strings such as 'All').
- * @type array
- * @default [ 10, 25, 50, 100 ]
- * @dtopt Option
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aLengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]]
- * } );
- * } );
- *
- * @example
- * // Setting the default display length as well as length menu
- * // This is likely to be wanted if you remove the '10' option which
- * // is the iDisplayLength default.
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "iDisplayLength": 25,
- * "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]]
- * } );
- * } );
- */
- "aLengthMenu": [ 10, 25, 50, 100 ],
-
-
- /**
- * The aoColumns option in the initialisation parameter allows you to define
- * details about the way individual columns behave. For a full list of
- * column options that can be set, please see
- * {@link DataTable.defaults.columns}. Note that if you use aoColumns to
- * define your columns, you must have an entry in the array for every single
- * column that you have in your table (these can be null if you don't which
- * to specify any options).
- * @member
- */
- "aoColumns": null,
-
- /**
- * Very similar to aoColumns, aoColumnDefs allows you to target a specific
- * column, multiple columns, or all columns, using the aTargets property of
- * each object in the array. This allows great flexibility when creating
- * tables, as the aoColumnDefs arrays can be of any length, targeting the
- * columns you specifically want. aoColumnDefs may use any of the column
- * options available: {@link DataTable.defaults.columns}, but it _must_
- * have aTargets defined in each object in the array. Values in the aTargets
- * array may be:
- *
- *
a string - class name will be matched on the TH for the column
- *
0 or a positive integer - column index counting from the left
- *
a negative integer - column index counting from the right
- *
the string "_all" - all columns (i.e. assign a default)
- *
- * @member
- */
- "aoColumnDefs": null,
-
-
- /**
- * Basically the same as oSearch, this parameter defines the individual column
- * filtering state at initialisation time. The array must be of the same size
- * as the number of columns, and each element be an object with the parameters
- * "sSearch" and "bEscapeRegex" (the latter is optional). 'null' is also
- * accepted and the default will be used.
- * @type array
- * @default []
- * @dtopt Option
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "aoSearchCols": [
- * null,
- * { "sSearch": "My filter" },
- * null,
- * { "sSearch": "^[0-9]", "bEscapeRegex": false }
- * ]
- * } );
- * } )
- */
- "aoSearchCols": [],
-
-
- /**
- * An array of CSS classes that should be applied to displayed rows. This
- * array may be of any length, and DataTables will apply each class
- * sequentially, looping when required.
- * @type array
- * @default null Will take the values determinted by the oClasses.sStripe*
- * options
- * @dtopt Option
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "asStripeClasses": [ 'strip1', 'strip2', 'strip3' ]
- * } );
- * } )
- */
- "asStripeClasses": null,
-
-
- /**
- * Enable or disable automatic column width calculation. This can be disabled
- * as an optimisation (it takes some time to calculate the widths) if the
- * tables widths are passed in using aoColumns.
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bAutoWidth": false
- * } );
- * } );
- */
- "bAutoWidth": true,
-
-
- /**
- * Deferred rendering can provide DataTables with a huge speed boost when you
- * are using an Ajax or JS data source for the table. This option, when set to
- * true, will cause DataTables to defer the creation of the table elements for
- * each row until they are needed for a draw - saving a significant amount of
- * time.
- * @type boolean
- * @default false
- * @dtopt Features
- *
- * @example
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable( {
- * "sAjaxSource": "sources/arrays.txt",
- * "bDeferRender": true
- * } );
- * } );
- */
- "bDeferRender": false,
-
-
- /**
- * Replace a DataTable which matches the given selector and replace it with
- * one which has the properties of the new initialisation object passed. If no
- * table matches the selector, then the new DataTable will be constructed as
- * per normal.
- * @type boolean
- * @default false
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sScrollY": "200px",
- * "bPaginate": false
- * } );
- *
- * // Some time later....
- * $('#example').dataTable( {
- * "bFilter": false,
- * "bDestroy": true
- * } );
- * } );
- */
- "bDestroy": false,
-
-
- /**
- * Enable or disable filtering of data. Filtering in DataTables is "smart" in
- * that it allows the end user to input multiple words (space separated) and
- * will match a row containing those words, even if not in the order that was
- * specified (this allow matching across multiple columns). Note that if you
- * wish to use filtering in DataTables this must remain 'true' - to remove the
- * default filtering input box and retain filtering abilities, please use
- * {@link DataTable.defaults.sDom}.
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bFilter": false
- * } );
- * } );
- */
- "bFilter": true,
-
-
- /**
- * Enable or disable the table information display. This shows information
- * about the data that is currently visible on the page, including information
- * about filtered data if that action is being performed.
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bInfo": false
- * } );
- * } );
- */
- "bInfo": true,
-
-
- /**
- * Enable jQuery UI ThemeRoller support (required as ThemeRoller requires some
- * slightly different and additional mark-up from what DataTables has
- * traditionally used).
- * @type boolean
- * @default false
- * @dtopt Features
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "bJQueryUI": true
- * } );
- * } );
- */
- "bJQueryUI": false,
-
-
- /**
- * Allows the end user to select the size of a formatted page from a select
- * menu (sizes are 10, 25, 50 and 100). Requires pagination (bPaginate).
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bLengthChange": false
- * } );
- * } );
- */
- "bLengthChange": true,
-
-
- /**
- * Enable or disable pagination.
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bPaginate": false
- * } );
- * } );
- */
- "bPaginate": true,
-
-
- /**
- * Enable or disable the display of a 'processing' indicator when the table is
- * being processed (e.g. a sort). This is particularly useful for tables with
- * large amounts of data where it can take a noticeable amount of time to sort
- * the entries.
- * @type boolean
- * @default false
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bProcessing": true
- * } );
- * } );
- */
- "bProcessing": false,
-
-
- /**
- * Retrieve the DataTables object for the given selector. Note that if the
- * table has already been initialised, this parameter will cause DataTables
- * to simply return the object that has already been set up - it will not take
- * account of any changes you might have made to the initialisation object
- * passed to DataTables (setting this parameter to true is an acknowledgement
- * that you understand this). bDestroy can be used to reinitialise a table if
- * you need.
- * @type boolean
- * @default false
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * initTable();
- * tableActions();
- * } );
- *
- * function initTable ()
- * {
- * return $('#example').dataTable( {
- * "sScrollY": "200px",
- * "bPaginate": false,
- * "bRetrieve": true
- * } );
- * }
- *
- * function tableActions ()
- * {
- * var oTable = initTable();
- * // perform API operations with oTable
- * }
- */
- "bRetrieve": false,
-
-
- /**
- * Indicate if DataTables should be allowed to set the padding / margin
- * etc for the scrolling header elements or not. Typically you will want
- * this.
- * @type boolean
- * @default true
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bScrollAutoCss": false,
- * "sScrollY": "200px"
- * } );
- * } );
- */
- "bScrollAutoCss": true,
-
-
- /**
- * When vertical (y) scrolling is enabled, DataTables will force the height of
- * the table's viewport to the given height at all times (useful for layout).
- * However, this can look odd when filtering data down to a small data set,
- * and the footer is left "floating" further down. This parameter (when
- * enabled) will cause DataTables to collapse the table's viewport down when
- * the result set will fit within the given Y height.
- * @type boolean
- * @default false
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sScrollY": "200",
- * "bScrollCollapse": true
- * } );
- * } );
- */
- "bScrollCollapse": false,
-
-
- /**
- * Enable infinite scrolling for DataTables (to be used in combination with
- * sScrollY). Infinite scrolling means that DataTables will continually load
- * data as a user scrolls through a table, which is very useful for large
- * dataset. This cannot be used with pagination, which is automatically
- * disabled. Note - the Scroller extra for DataTables is recommended in
- * in preference to this option.
- * @type boolean
- * @default false
- * @dtopt Features
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bScrollInfinite": true,
- * "bScrollCollapse": true,
- * "sScrollY": "200px"
- * } );
- * } );
- */
- "bScrollInfinite": false,
-
-
- /**
- * Configure DataTables to use server-side processing. Note that the
- * sAjaxSource parameter must also be given in order to give DataTables a
- * source to obtain the required data for each draw.
- * @type boolean
- * @default false
- * @dtopt Features
- * @dtopt Server-side
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bServerSide": true,
- * "sAjaxSource": "xhr.php"
- * } );
- * } );
- */
- "bServerSide": false,
-
-
- /**
- * Enable or disable sorting of columns. Sorting of individual columns can be
- * disabled by the "bSortable" option for each column.
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bSort": false
- * } );
- * } );
- */
- "bSort": true,
-
-
- /**
- * Allows control over whether DataTables should use the top (true) unique
- * cell that is found for a single column, or the bottom (false - default).
- * This is useful when using complex headers.
- * @type boolean
- * @default false
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bSortCellsTop": true
- * } );
- * } );
- */
- "bSortCellsTop": false,
-
-
- /**
- * Enable or disable the addition of the classes 'sorting_1', 'sorting_2' and
- * 'sorting_3' to the columns which are currently being sorted on. This is
- * presented as a feature switch as it can increase processing time (while
- * classes are removed and added) so for large data sets you might want to
- * turn this off.
- * @type boolean
- * @default true
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bSortClasses": false
- * } );
- * } );
- */
- "bSortClasses": true,
-
-
- /**
- * Enable or disable state saving. When enabled a cookie will be used to save
- * table display information such as pagination information, display length,
- * filtering and sorting. As such when the end user reloads the page the
- * display display will match what thy had previously set up.
- * @type boolean
- * @default false
- * @dtopt Features
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "bStateSave": true
- * } );
- * } );
- */
- "bStateSave": false,
-
-
- /**
- * Customise the cookie and / or the parameters being stored when using
- * DataTables with state saving enabled. This function is called whenever
- * the cookie is modified, and it expects a fully formed cookie string to be
- * returned. Note that the data object passed in is a Javascript object which
- * must be converted to a string (JSON.stringify for example).
- * @type function
- * @param {string} sName Name of the cookie defined by DataTables
- * @param {object} oData Data to be stored in the cookie
- * @param {string} sExpires Cookie expires string
- * @param {string} sPath Path of the cookie to set
- * @returns {string} Cookie formatted string (which should be encoded by
- * using encodeURIComponent())
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready( function () {
- * $('#example').dataTable( {
- * "fnCookieCallback": function (sName, oData, sExpires, sPath) {
- * // Customise oData or sName or whatever else here
- * return sName + "="+JSON.stringify(oData)+"; expires=" + sExpires +"; path=" + sPath;
- * }
- * } );
- * } );
- */
- "fnCookieCallback": null,
-
-
- /**
- * This function is called when a TR element is created (and all TD child
- * elements have been inserted), or registered if using a DOM source, allowing
- * manipulation of the TR element (adding classes etc).
- * @type function
- * @param {node} nRow "TR" element for the current row
- * @param {array} aData Raw data array for this row
- * @param {int} iDataIndex The index of this row in aoData
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "fnCreatedRow": function( nRow, aData, iDataIndex ) {
- * // Bold the grade for all 'A' grade browsers
- * if ( aData[4] == "A" )
- * {
- * $('td:eq(4)', nRow).html( 'A' );
- * }
- * }
- * } );
- * } );
- */
- "fnCreatedRow": null,
-
-
- /**
- * This function is called on every 'draw' event, and allows you to
- * dynamically modify any aspect you want about the created DOM.
- * @type function
- * @param {object} oSettings DataTables settings object
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "fnDrawCallback": function( oSettings ) {
- * alert( 'DataTables has redrawn the table' );
- * }
- * } );
- * } );
- */
- "fnDrawCallback": null,
-
-
- /**
- * Identical to fnHeaderCallback() but for the table footer this function
- * allows you to modify the table footer on every 'draw' even.
- * @type function
- * @param {node} nFoot "TR" element for the footer
- * @param {array} aData Full table data (as derived from the original HTML)
- * @param {int} iStart Index for the current display starting point in the
- * display array
- * @param {int} iEnd Index for the current display ending point in the
- * display array
- * @param {array int} aiDisplay Index array to translate the visual position
- * to the full data array
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "fnFooterCallback": function( nFoot, aData, iStart, iEnd, aiDisplay ) {
- * nFoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+iStart;
- * }
- * } );
- * } )
- */
- "fnFooterCallback": null,
-
-
- /**
- * When rendering large numbers in the information element for the table
- * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers
- * to have a comma separator for the 'thousands' units (e.g. 1 million is
- * rendered as "1,000,000") to help readability for the end user. This
- * function will override the default method DataTables uses.
- * @type function
- * @member
- * @param {int} iIn number to be formatted
- * @returns {string} formatted string for DataTables to show the number
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "fnFormatNumber": function ( iIn ) {
- * if ( iIn < 1000 ) {
- * return iIn;
- * } else {
- * var
- * s=(iIn+""),
- * a=s.split(""), out="",
- * iLen=s.length;
- *
- * for ( var i=0 ; i<iLen ; i++ ) {
- * if ( i%3 === 0 && i !== 0 ) {
- * out = "'"+out;
- * }
- * out = a[iLen-i-1]+out;
- * }
- * }
- * return out;
- * };
- * } );
- * } );
- */
- "fnFormatNumber": function ( iIn ) {
- if ( iIn < 1000 )
- {
- // A small optimisation for what is likely to be the majority of use cases
- return iIn;
- }
-
- var s=(iIn+""), a=s.split(""), out="", iLen=s.length;
-
- for ( var i=0 ; iA' );
- * }
- * }
- * } );
- * } );
- */
- "fnRowCallback": null,
-
-
- /**
- * This parameter allows you to override the default function which obtains
- * the data from the server ($.getJSON) so something more suitable for your
- * application. For example you could use POST data, or pull information from
- * a Gears or AIR database.
- * @type function
- * @member
- * @param {string} sSource HTTP source to obtain the data from (sAjaxSource)
- * @param {array} aoData A key/value pair object containing the data to send
- * to the server
- * @param {function} fnCallback to be called on completion of the data get
- * process that will draw the data on the page.
- * @param {object} oSettings DataTables settings object
- * @dtopt Callbacks
- * @dtopt Server-side
- *
- * @example
- * // POST data to server
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bProcessing": true,
- * "bServerSide": true,
- * "sAjaxSource": "xhr.php",
- * "fnServerData": function ( sSource, aoData, fnCallback ) {
- * $.ajax( {
- * "dataType": 'json',
- * "type": "POST",
- * "url": sSource,
- * "data": aoData,
- * "success": fnCallback
- * } );
- * }
- * } );
- * } );
- */
- "fnServerData": function ( sUrl, aoData, fnCallback, oSettings ) {
- oSettings.jqXHR = $.ajax( {
- "url": sUrl,
- "data": aoData,
- "success": function (json) {
- $(oSettings.oInstance).trigger('xhr', oSettings);
- fnCallback( json );
- },
- "dataType": "json",
- "cache": false,
- "type": oSettings.sServerMethod,
- "error": function (xhr, error, thrown) {
- if ( error == "parsererror" ) {
- oSettings.oApi._fnLog( oSettings, 0, "DataTables warning: JSON data from "+
- "server could not be parsed. This is caused by a JSON formatting error." );
- }
- }
- } );
- },
-
-
- /**
- * It is often useful to send extra data to the server when making an Ajax
- * request - for example custom filtering information, and this callback
- * function makes it trivial to send extra information to the server. The
- * passed in parameter is the data set that has been constructed by
- * DataTables, and you can add to this or modify it as you require.
- * @type function
- * @param {array} aoData Data array (array of objects which are name/value
- * pairs) that has been constructed by DataTables and will be sent to the
- * server. In the case of Ajax sourced data with server-side processing
- * this will be an empty array, for server-side processing there will be a
- * significant number of parameters!
- * @returns {undefined} Ensure that you modify the aoData array passed in,
- * as this is passed by reference.
- * @dtopt Callbacks
- * @dtopt Server-side
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bProcessing": true,
- * "bServerSide": true,
- * "sAjaxSource": "scripts/server_processing.php",
- * "fnServerParams": function ( aoData ) {
- * aoData.push( { "name": "more_data", "value": "my_value" } );
- * }
- * } );
- * } );
- */
- "fnServerParams": null,
-
-
- /**
- * Load the table state. With this function you can define from where, and how, the
- * state of a table is loaded. By default DataTables will load from its state saving
- * cookie, but you might wish to use local storage (HTML5) or a server-side database.
- * @type function
- * @member
- * @param {object} oSettings DataTables settings object
- * @return {object} The DataTables state object to be loaded
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bStateSave": true,
- * "fnStateLoad": function (oSettings, oData) {
- * var o;
- *
- * // Send an Ajax request to the server to get the data. Note that
- * // this is a synchronous request.
- * $.ajax( {
- * "url": "/state_load",
- * "async": false,
- * "dataType": "json",
- * "success": function (json) {
- * o = json;
- * }
- * } );
- *
- * return o;
- * }
- * } );
- * } );
- */
- "fnStateLoad": function ( oSettings ) {
- var sData = this.oApi._fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );
- var oData;
-
- try {
- oData = (typeof $.parseJSON === 'function') ?
- $.parseJSON(sData) : eval( '('+sData+')' );
- } catch (e) {
- oData = null;
- }
-
- return oData;
- },
-
-
- /**
- * Callback which allows modification of the saved state prior to loading that state.
- * This callback is called when the table is loading state from the stored data, but
- * prior to the settings object being modified by the saved state. Note that for
- * plug-in authors, you should use the 'stateLoadParams' event to load parameters for
- * a plug-in.
- * @type function
- * @param {object} oSettings DataTables settings object
- * @param {object} oData The state object that is to be loaded
- * @dtopt Callbacks
- *
- * @example
- * // Remove a saved filter, so filtering is never loaded
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bStateSave": true,
- * "fnStateLoadParams": function (oSettings, oData) {
- * oData.oSearch.sSearch = "";
- * } );
- * } );
- *
- * @example
- * // Disallow state loading by returning false
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bStateSave": true,
- * "fnStateLoadParams": function (oSettings, oData) {
- * return false;
- * } );
- * } );
- */
- "fnStateLoadParams": null,
-
-
- /**
- * Callback that is called when the state has been loaded from the state saving method
- * and the DataTables settings object has been modified as a result of the loaded state.
- * @type function
- * @param {object} oSettings DataTables settings object
- * @param {object} oData The state object that was loaded
- * @dtopt Callbacks
- *
- * @example
- * // Show an alert with the filtering value that was saved
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bStateSave": true,
- * "fnStateLoaded": function (oSettings, oData) {
- * alert( 'Saved filter was: '+oData.oSearch.sSearch );
- * } );
- * } );
- */
- "fnStateLoaded": null,
-
-
- /**
- * Save the table state. This function allows you to define where and how the state
- * information for the table is stored - by default it will use a cookie, but you
- * might want to use local storage (HTML5) or a server-side database.
- * @type function
- * @member
- * @param {object} oSettings DataTables settings object
- * @param {object} oData The state object to be saved
- * @dtopt Callbacks
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bStateSave": true,
- * "fnStateSave": function (oSettings, oData) {
- * // Send an Ajax request to the server with the state object
- * $.ajax( {
- * "url": "/state_save",
- * "data": oData,
- * "dataType": "json",
- * "method": "POST"
- * "success": function () {}
- * } );
- * }
- * } );
- * } );
- */
- "fnStateSave": function ( oSettings, oData ) {
- this.oApi._fnCreateCookie(
- oSettings.sCookiePrefix+oSettings.sInstance,
- this.oApi._fnJsonString(oData),
- oSettings.iCookieDuration,
- oSettings.sCookiePrefix,
- oSettings.fnCookieCallback
- );
- },
-
-
- /**
- * Callback which allows modification of the state to be saved. Called when the table
- * has changed state a new state save is required. This method allows modification of
- * the state saving object prior to actually doing the save, including addition or
- * other state properties or modification. Note that for plug-in authors, you should
- * use the 'stateSaveParams' event to save parameters for a plug-in.
- * @type function
- * @param {object} oSettings DataTables settings object
- * @param {object} oData The state object to be saved
- * @dtopt Callbacks
- *
- * @example
- * // Remove a saved filter, so filtering is never saved
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bStateSave": true,
- * "fnStateSaveParams": function (oSettings, oData) {
- * oData.oSearch.sSearch = "";
- * } );
- * } );
- */
- "fnStateSaveParams": null,
-
-
- /**
- * Duration of the cookie which is used for storing session information. This
- * value is given in seconds.
- * @type int
- * @default 7200 (2 hours)
- * @dtopt Options
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "iCookieDuration": 60*60*24 // 1 day
- * } );
- * } )
- */
- "iCookieDuration": 7200,
-
-
- /**
- * When enabled DataTables will not make a request to the server for the first
- * page draw - rather it will use the data already on the page (no sorting etc
- * will be applied to it), thus saving on an XHR at load time. iDeferLoading
- * is used to indicate that deferred loading is required, but it is also used
- * to tell DataTables how many records there are in the full table (allowing
- * the information element and pagination to be displayed correctly). In the case
- * where a filtering is applied to the table on initial load, this can be
- * indicated by giving the parameter as an array, where the first element is
- * the number of records available after filtering and the second element is the
- * number of records without filtering (allowing the table information element
- * to be shown correctly).
- * @type int | array
- * @default null
- * @dtopt Options
- *
- * @example
- * // 57 records available in the table, no filtering applied
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bServerSide": true,
- * "sAjaxSource": "scripts/server_processing.php",
- * "iDeferLoading": 57
- * } );
- * } );
- *
- * @example
- * // 57 records after filtering, 100 without filtering (an initial filter applied)
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bServerSide": true,
- * "sAjaxSource": "scripts/server_processing.php",
- * "iDeferLoading": [ 57, 100 ],
- * "oSearch": {
- * "sSearch": "my_filter"
- * }
- * } );
- * } );
- */
- "iDeferLoading": null,
-
-
- /**
- * Number of rows to display on a single page when using pagination. If
- * feature enabled (bLengthChange) then the end user will be able to override
- * this to a custom setting using a pop-up menu.
- * @type int
- * @default 10
- * @dtopt Options
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "iDisplayLength": 50
- * } );
- * } )
- */
- "iDisplayLength": 10,
-
-
- /**
- * Define the starting point for data display when using DataTables with
- * pagination. Note that this parameter is the number of records, rather than
- * the page number, so if you have 10 records per page and want to start on
- * the third page, it should be "20".
- * @type int
- * @default 0
- * @dtopt Options
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "iDisplayStart": 20
- * } );
- * } )
- */
- "iDisplayStart": 0,
-
-
- /**
- * The scroll gap is the amount of scrolling that is left to go before
- * DataTables will load the next 'page' of data automatically. You typically
- * want a gap which is big enough that the scrolling will be smooth for the
- * user, while not so large that it will load more data than need.
- * @type int
- * @default 100
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bScrollInfinite": true,
- * "bScrollCollapse": true,
- * "sScrollY": "200px",
- * "iScrollLoadGap": 50
- * } );
- * } );
- */
- "iScrollLoadGap": 100,
-
-
- /**
- * By default DataTables allows keyboard navigation of the table (sorting, paging,
- * and filtering) by adding a tabindex attribute to the required elements. This
- * allows you to tab through the controls and press the enter key to activate them.
- * The tabindex is default 0, meaning that the tab follows the flow of the document.
- * You can overrule this using this parameter if you wish. Use a value of -1 to
- * disable built-in keyboard navigation.
- * @type int
- * @default 0
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "iTabIndex": 1
- * } );
- * } );
- */
- "iTabIndex": 0,
-
-
- /**
- * All strings that DataTables uses in the user interface that it creates
- * are defined in this object, allowing you to modified them individually or
- * completely replace them all as required.
- * @namespace
- */
- "oLanguage": {
- /**
- * Strings that are used for WAI-ARIA labels and controls only (these are not
- * actually visible on the page, but will be read by screenreaders, and thus
- * must be internationalised as well).
- * @namespace
- */
- "oAria": {
- /**
- * ARIA label that is added to the table headers when the column may be
- * sorted ascending by activing the column (click or return when focused).
- * Note that the column header is prefixed to this string.
- * @type string
- * @default : activate to sort column ascending
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "oAria": {
- * "sSortAscending": " - click/return to sort ascending"
- * }
- * }
- * } );
- * } );
- */
- "sSortAscending": ": activate to sort column ascending",
-
- /**
- * ARIA label that is added to the table headers when the column may be
- * sorted descending by activing the column (click or return when focused).
- * Note that the column header is prefixed to this string.
- * @type string
- * @default : activate to sort column ascending
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "oAria": {
- * "sSortDescending": " - click/return to sort descending"
- * }
- * }
- * } );
- * } );
- */
- "sSortDescending": ": activate to sort column descending"
- },
-
- /**
- * Pagination string used by DataTables for the two built-in pagination
- * control types ("two_button" and "full_numbers")
- * @namespace
- */
- "oPaginate": {
- /**
- * Text to use when using the 'full_numbers' type of pagination for the
- * button to take the user to the first page.
- * @type string
- * @default First
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "oPaginate": {
- * "sFirst": "First page"
- * }
- * }
- * } );
- * } );
- */
- "sFirst": "First",
-
-
- /**
- * Text to use when using the 'full_numbers' type of pagination for the
- * button to take the user to the last page.
- * @type string
- * @default Last
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "oPaginate": {
- * "sLast": "Last page"
- * }
- * }
- * } );
- * } );
- */
- "sLast": "Last",
-
-
- /**
- * Text to use when using the 'full_numbers' type of pagination for the
- * button to take the user to the next page.
- * @type string
- * @default Next
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "oPaginate": {
- * "sNext": "Next page"
- * }
- * }
- * } );
- * } );
- */
- "sNext": "Next",
-
-
- /**
- * Text to use when using the 'full_numbers' type of pagination for the
- * button to take the user to the previous page.
- * @type string
- * @default Previous
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "oPaginate": {
- * "sPrevious": "Previous page"
- * }
- * }
- * } );
- * } );
- */
- "sPrevious": "Previous"
- },
-
- /**
- * This string is shown in preference to sZeroRecords when the table is
- * empty of data (regardless of filtering). Note that this is an optional
- * parameter - if it is not given, the value of sZeroRecords will be used
- * instead (either the default or given value).
- * @type string
- * @default No data available in table
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sEmptyTable": "No data available in table"
- * }
- * } );
- * } );
- */
- "sEmptyTable": "No data available in table",
-
-
- /**
- * This string gives information to the end user about the information that
- * is current on display on the page. The _START_, _END_ and _TOTAL_
- * variables are all dynamically replaced as the table display updates, and
- * can be freely moved or removed as the language requirements change.
- * @type string
- * @default Showing _START_ to _END_ of _TOTAL_ entries
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sInfo": "Got a total of _TOTAL_ entries to show (_START_ to _END_)"
- * }
- * } );
- * } );
- */
- "sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
-
-
- /**
- * Display information string for when the table is empty. Typically the
- * format of this string should match sInfo.
- * @type string
- * @default Showing 0 to 0 of 0 entries
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sInfoEmpty": "No entries to show"
- * }
- * } );
- * } );
- */
- "sInfoEmpty": "Showing 0 to 0 of 0 entries",
-
-
- /**
- * When a user filters the information in a table, this string is appended
- * to the information (sInfo) to give an idea of how strong the filtering
- * is. The variable _MAX_ is dynamically updated.
- * @type string
- * @default (filtered from _MAX_ total entries)
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sInfoFiltered": " - filtering from _MAX_ records"
- * }
- * } );
- * } );
- */
- "sInfoFiltered": "(filtered from _MAX_ total entries)",
-
-
- /**
- * If can be useful to append extra information to the info string at times,
- * and this variable does exactly that. This information will be appended to
- * the sInfo (sInfoEmpty and sInfoFiltered in whatever combination they are
- * being used) at all times.
- * @type string
- * @default Empty string
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sInfoPostFix": "All records shown are derived from real information."
- * }
- * } );
- * } );
- */
- "sInfoPostFix": "",
-
-
- /**
- * DataTables has a build in number formatter (fnFormatNumber) which is used
- * to format large numbers that are used in the table information. By
- * default a comma is used, but this can be trivially changed to any
- * character you wish with this parameter.
- * @type string
- * @default ,
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sInfoThousands": "'"
- * }
- * } );
- * } );
- */
- "sInfoThousands": ",",
-
-
- /**
- * Detail the action that will be taken when the drop down menu for the
- * pagination length option is changed. The '_MENU_' variable is replaced
- * with a default select list of 10, 25, 50 and 100, and can be replaced
- * with a custom select box if required.
- * @type string
- * @default Show _MENU_ entries
- * @dtopt Language
- *
- * @example
- * // Language change only
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sLengthMenu": "Display _MENU_ records"
- * }
- * } );
- * } );
- *
- * @example
- * // Language and options change
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sLengthMenu": 'Display records'
- * }
- * } );
- * } );
- */
- "sLengthMenu": "Show _MENU_ entries",
-
-
- /**
- * When using Ajax sourced data and during the first draw when DataTables is
- * gathering the data, this message is shown in an empty row in the table to
- * indicate to the end user the the data is being loaded. Note that this
- * parameter is not used when loading data by server-side processing, just
- * Ajax sourced data with client-side processing.
- * @type string
- * @default Loading...
- * @dtopt Language
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sLoadingRecords": "Please wait - loading..."
- * }
- * } );
- * } );
- */
- "sLoadingRecords": "Loading...",
-
-
- /**
- * Text which is displayed when the table is processing a user action
- * (usually a sort command or similar).
- * @type string
- * @default Processing...
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sProcessing": "DataTables is currently busy"
- * }
- * } );
- * } );
- */
- "sProcessing": "Processing...",
-
-
- /**
- * Details the actions that will be taken when the user types into the
- * filtering input text box. The variable "_INPUT_", if used in the string,
- * is replaced with the HTML text box for the filtering input allowing
- * control over where it appears in the string. If "_INPUT_" is not given
- * then the input box is appended to the string automatically.
- * @type string
- * @default Search:
- * @dtopt Language
- *
- * @example
- * // Input text box will be appended at the end automatically
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sSearch": "Filter records:"
- * }
- * } );
- * } );
- *
- * @example
- * // Specify where the filter should appear
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sSearch": "Apply filter _INPUT_ to table"
- * }
- * } );
- * } );
- */
- "sSearch": "Search:",
-
-
- /**
- * All of the language information can be stored in a file on the
- * server-side, which DataTables will look up if this parameter is passed.
- * It must store the URL of the language file, which is in a JSON format,
- * and the object has the same properties as the oLanguage object in the
- * initialiser object (i.e. the above parameters). Please refer to one of
- * the example language files to see how this works in action.
- * @type string
- * @default Empty string - i.e. disabled
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sUrl": "http://www.sprymedia.co.uk/dataTables/lang.txt"
- * }
- * } );
- * } );
- */
- "sUrl": "",
-
-
- /**
- * Text shown inside the table records when the is no information to be
- * displayed after filtering. sEmptyTable is shown when there is simply no
- * information in the table at all (regardless of filtering).
- * @type string
- * @default No matching records found
- * @dtopt Language
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "oLanguage": {
- * "sZeroRecords": "No records to display"
- * }
- * } );
- * } );
- */
- "sZeroRecords": "No matching records found"
- },
-
-
- /**
- * This parameter allows you to have define the global filtering state at
- * initialisation time. As an object the "sSearch" parameter must be
- * defined, but all other parameters are optional. When "bRegex" is true,
- * the search string will be treated as a regular expression, when false
- * (default) it will be treated as a straight string. When "bSmart"
- * DataTables will use it's smart filtering methods (to word match at
- * any point in the data), when false this will not be done.
- * @namespace
- * @extends DataTable.models.oSearch
- * @dtopt Options
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "oSearch": {"sSearch": "Initial search"}
- * } );
- * } )
- */
- "oSearch": $.extend( {}, DataTable.models.oSearch ),
-
-
- /**
- * By default DataTables will look for the property 'aaData' when obtaining
- * data from an Ajax source or for server-side processing - this parameter
- * allows that property to be changed. You can use Javascript dotted object
- * notation to get a data source for multiple levels of nesting.
- * @type string
- * @default aaData
- * @dtopt Options
- * @dtopt Server-side
- *
- * @example
- * // Get data from { "data": [...] }
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable( {
- * "sAjaxSource": "sources/data.txt",
- * "sAjaxDataProp": "data"
- * } );
- * } );
- *
- * @example
- * // Get data from { "data": { "inner": [...] } }
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable( {
- * "sAjaxSource": "sources/data.txt",
- * "sAjaxDataProp": "data.inner"
- * } );
- * } );
- */
- "sAjaxDataProp": "aaData",
-
-
- /**
- * You can instruct DataTables to load data from an external source using this
- * parameter (use aData if you want to pass data in you already have). Simply
- * provide a url a JSON object can be obtained from. This object must include
- * the parameter 'aaData' which is the data source for the table.
- * @type string
- * @default null
- * @dtopt Options
- * @dtopt Server-side
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "sAjaxSource": "http://www.sprymedia.co.uk/dataTables/json.php"
- * } );
- * } )
- */
- "sAjaxSource": null,
-
-
- /**
- * This parameter can be used to override the default prefix that DataTables
- * assigns to a cookie when state saving is enabled.
- * @type string
- * @default SpryMedia_DataTables_
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sCookiePrefix": "my_datatable_",
- * } );
- * } );
- */
- "sCookiePrefix": "SpryMedia_DataTables_",
-
-
- /**
- * This initialisation variable allows you to specify exactly where in the
- * DOM you want DataTables to inject the various controls it adds to the page
- * (for example you might want the pagination controls at the top of the
- * table). DIV elements (with or without a custom class) can also be added to
- * aid styling. The follow syntax is used:
- *
- *
- * @type string
- * @default lfrtip (when bJQueryUI is false)or
- * <"H"lfr>t<"F"ip> (when bJQueryUI is true)
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sDom": '<"top"i>rt<"bottom"flp><"clear"&lgt;'
- * } );
- * } );
- */
- "sDom": "lfrtip",
-
-
- /**
- * DataTables features two different built-in pagination interaction methods
- * ('two_button' or 'full_numbers') which present different page controls to
- * the end user. Further methods can be added using the API (see below).
- * @type string
- * @default two_button
- * @dtopt Options
- *
- * @example
- * $(document).ready( function() {
- * $('#example').dataTable( {
- * "sPaginationType": "full_numbers"
- * } );
- * } )
- */
- "sPaginationType": "two_button",
-
-
- /**
- * Enable horizontal scrolling. When a table is too wide to fit into a certain
- * layout, or you have a large number of columns in the table, you can enable
- * x-scrolling to show the table in a viewport, which can be scrolled. This
- * property can be any CSS unit, or a number (in which case it will be treated
- * as a pixel measurement).
- * @type string
- * @default blank string - i.e. disabled
- * @dtopt Features
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sScrollX": "100%",
- * "bScrollCollapse": true
- * } );
- * } );
- */
- "sScrollX": "",
-
-
- /**
- * This property can be used to force a DataTable to use more width than it
- * might otherwise do when x-scrolling is enabled. For example if you have a
- * table which requires to be well spaced, this parameter is useful for
- * "over-sizing" the table, and thus forcing scrolling. This property can by
- * any CSS unit, or a number (in which case it will be treated as a pixel
- * measurement).
- * @type string
- * @default blank string - i.e. disabled
- * @dtopt Options
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sScrollX": "100%",
- * "sScrollXInner": "110%"
- * } );
- * } );
- */
- "sScrollXInner": "",
-
-
- /**
- * Enable vertical scrolling. Vertical scrolling will constrain the DataTable
- * to the given height, and enable scrolling for any data which overflows the
- * current viewport. This can be used as an alternative to paging to display
- * a lot of data in a small area (although paging and scrolling can both be
- * enabled at the same time). This property can be any CSS unit, or a number
- * (in which case it will be treated as a pixel measurement).
- * @type string
- * @default blank string - i.e. disabled
- * @dtopt Features
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "sScrollY": "200px",
- * "bPaginate": false
- * } );
- * } );
- */
- "sScrollY": "",
-
-
- /**
- * Set the HTTP method that is used to make the Ajax call for server-side
- * processing or Ajax sourced data.
- * @type string
- * @default GET
- * @dtopt Options
- * @dtopt Server-side
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "bServerSide": true,
- * "sAjaxSource": "scripts/post.php",
- * "sServerMethod": "POST"
- * } );
- * } );
- */
- "sServerMethod": "GET"
- };
-
-
-
- /**
- * Column options that can be given to DataTables at initialisation time.
- * @namespace
- */
- DataTable.defaults.columns = {
- /**
- * Allows a column's sorting to take multiple columns into account when
- * doing a sort. For example first name / last name columns make sense to
- * do a multi-column sort over the two columns.
- * @type array
- * @default null Takes the value of the column index automatically
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "aDataSort": [ 0, 1 ], "aTargets": [ 0 ] },
- * { "aDataSort": [ 1, 0 ], "aTargets": [ 1 ] },
- * { "aDataSort": [ 2, 3, 4 ], "aTargets": [ 2 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "aDataSort": [ 0, 1 ] },
- * { "aDataSort": [ 1, 0 ] },
- * { "aDataSort": [ 2, 3, 4 ] },
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "aDataSort": null,
-
-
- /**
- * You can control the default sorting direction, and even alter the behaviour
- * of the sort handler (i.e. only allow ascending sorting etc) using this
- * parameter.
- * @type array
- * @default [ 'asc', 'desc' ]
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "asSorting": [ "asc" ], "aTargets": [ 1 ] },
- * { "asSorting": [ "desc", "asc", "asc" ], "aTargets": [ 2 ] },
- * { "asSorting": [ "desc" ], "aTargets": [ 3 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * null,
- * { "asSorting": [ "asc" ] },
- * { "asSorting": [ "desc", "asc", "asc" ] },
- * { "asSorting": [ "desc" ] },
- * null
- * ]
- * } );
- * } );
- */
- "asSorting": [ 'asc', 'desc' ],
-
-
- /**
- * Enable or disable filtering on the data in this column.
- * @type boolean
- * @default true
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "bSearchable": false, "aTargets": [ 0 ] }
- * ] } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "bSearchable": false },
- * null,
- * null,
- * null,
- * null
- * ] } );
- * } );
- */
- "bSearchable": true,
-
-
- /**
- * Enable or disable sorting on this column.
- * @type boolean
- * @default true
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "bSortable": false, "aTargets": [ 0 ] }
- * ] } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "bSortable": false },
- * null,
- * null,
- * null,
- * null
- * ] } );
- * } );
- */
- "bSortable": true,
-
-
- /**
- * When using fnRender() for a column, you may wish to use the original data
- * (before rendering) for sorting and filtering (the default is to used the
- * rendered data that the user can see). This may be useful for dates etc.
- *
- * *NOTE* It is it is advisable now to use mDataProp as a function and make
- * use of the 'type' that it gives, allowing (potentially) different data to
- * be used for sorting, filtering, display and type detection.
- * @type boolean
- * @default true
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * {
- * "fnRender": function ( oObj ) {
- * return oObj.aData[0] +' '+ oObj.aData[3];
- * },
- * "bUseRendered": false,
- * "aTargets": [ 0 ]
- * }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * {
- * "fnRender": function ( oObj ) {
- * return oObj.aData[0] +' '+ oObj.aData[3];
- * },
- * "bUseRendered": false
- * },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "bUseRendered": true,
-
-
- /**
- * Enable or disable the display of this column.
- * @type boolean
- * @default true
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "bVisible": false, "aTargets": [ 0 ] }
- * ] } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "bVisible": false },
- * null,
- * null,
- * null,
- * null
- * ] } );
- * } );
- */
- "bVisible": true,
-
-
- /**
- * Developer definable function that is called whenever a cell is created (Ajax source,
- * etc) or processed for input (DOM source). This can be used as a compliment to fnRender
- * allowing you to modify the DOM element (add background colour for example) when the
- * element is available (since it is not when fnRender is called).
- * @type function
- * @param {element} nTd The TD node that has been created
- * @param {*} sData The Data for the cell
- * @param {array|object} oData The data for the whole row
- * @param {int} iRow The row index for the aoData data store
- * @param {int} iCol The column index for aoColumns
- * @dtopt Columns
- *
- * @example
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [ {
- * "aTargets": [3],
- * "fnCreatedCell": function (nTd, sData, oData, iRow, iCol) {
- * if ( sData == "1.7" ) {
- * $(nTd).css('color', 'blue')
- * }
- * }
- * } ]
- * });
- * } );
- */
- "fnCreatedCell": null,
-
-
- /**
- * Custom display function that will be called for the display of each cell in
- * this column.
- * @type function
- * @param {object} o Object with the following parameters:
- * @param {int} o.iDataRow The row in aoData
- * @param {int} o.iDataColumn The column in question
- * @param {array} o.aData The data for the row in question
- * @param {object} o.oSettings The settings object for this DataTables instance
- * @param {object} o.mDataProp The data property used for this column
- * @param {*} val The current cell value
- * @returns {string} The string you which to use in the display
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * {
- * "fnRender": function ( o, val ) {
- * return o.aData[0] +' '+ o.aData[3];
- * },
- * "aTargets": [ 0 ]
- * }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "fnRender": function ( o, val ) {
- * return o.aData[0] +' '+ o.aData[3];
- * } },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "fnRender": null,
-
-
- /**
- * The column index (starting from 0!) that you wish a sort to be performed
- * upon when this column is selected for sorting. This can be used for sorting
- * on hidden columns for example.
- * @type int
- * @default -1 Use automatically calculated column index
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "iDataSort": 1, "aTargets": [ 0 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "iDataSort": 1 },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "iDataSort": -1,
-
-
- /**
- * This property can be used to read data from any JSON data source property,
- * including deeply nested objects / properties. mDataProp can be given in a
- * number of different ways which effect its behaviour:
- *
- *
integer - treated as an array index for the data source. This is the
- * default that DataTables uses (incrementally increased for each column).
- *
string - read an object property from the data source. Note that you can
- * use Javascript dotted notation to read deep properties/arrays from the
- * data source.
- *
null - the sDefaultContent option will be used for the cell (null
- * by default, so you will need to specify the default content you want -
- * typically an empty string). This can be useful on generated columns such
- * as edit / delete action columns.
- *
function - the function given will be executed whenever DataTables
- * needs to set or get the data for a cell in the column. The function
- * takes three parameters:
- *
- *
{array|object} The data source for the row
- *
{string} The type call data requested - this will be 'set' when
- * setting data or 'filter', 'display', 'type', 'sort' or undefined when
- * gathering data. Note that when undefined is given for the type
- * DataTables expects to get the raw data for the object back
- *
{*} Data to set when the second parameter is 'set'.
- *
- * The return value from the function is not required when 'set' is the type
- * of call, but otherwise the return is what will be used for the data
- * requested.
- *
- * @type string|int|function|null
- * @default null Use automatically calculated column index
- * @dtopt Columns
- *
- * @example
- * // Read table data from objects
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable( {
- * "sAjaxSource": "sources/deep.txt",
- * "aoColumns": [
- * { "mDataProp": "engine" },
- * { "mDataProp": "browser" },
- * { "mDataProp": "platform.inner" },
- * { "mDataProp": "platform.details.0" },
- * { "mDataProp": "platform.details.1" }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using mDataProp as a function to provide different information for
- * // sorting, filtering and display. In this case, currency (price)
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable( {
- * "aoColumnDefs": [
- * {
- * "aTargets": [ 0 ],
- * "mDataProp": function ( source, type, val ) {
- * if (type === 'set') {
- * source.price = val;
- * // Store the computed dislay and filter values for efficiency
- * source.price_display = val=="" ? "" : "$"+numberFormat(val);
- * source.price_filter = val=="" ? "" : "$"+numberFormat(val)+" "+val;
- * return;
- * }
- * else if (type === 'display') {
- * return source.price_display;
- * }
- * else if (type === 'filter') {
- * return source.price_filter;
- * }
- * // 'sort', 'type' and undefined all just use the integer
- * return source.price;
- * }
- * ]
- * } );
- * } );
- */
- "mDataProp": null,
-
-
- /**
- * Change the cell type created for the column - either TD cells or TH cells. This
- * can be useful as TH cells have semantic meaning in the table body, allowing them
- * to act as a header for a row (you may wish to add scope='row' to the TH elements).
- * @type string
- * @default td
- * @dtopt Columns
- *
- * @example
- * // Make the first column use TH cells
- * $(document).ready(function() {
- * var oTable = $('#example').dataTable( {
- * "aoColumnDefs": [
- * {
- * "aTargets": [ 0 ],
- * "sCellType": "th"
- * ]
- * } );
- * } );
- */
- "sCellType": "td",
-
-
- /**
- * Class to give to each cell in this column.
- * @type string
- * @default Empty string
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "sClass": "my_class", "aTargets": [ 0 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "sClass": "my_class" },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "sClass": "",
-
- /**
- * When DataTables calculates the column widths to assign to each column,
- * it finds the longest string in each column and then constructs a
- * temporary table and reads the widths from that. The problem with this
- * is that "mmm" is much wider then "iiii", but the latter is a longer
- * string - thus the calculation can go wrong (doing it properly and putting
- * it into an DOM object and measuring that is horribly(!) slow). Thus as
- * a "work around" we provide this option. It will append its value to the
- * text that is found to be the longest string for the column - i.e. padding.
- * Generally you shouldn't need this, and it is not documented on the
- * general DataTables.net documentation
- * @type string
- * @default Empty string
- * @dtopt Columns
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * null,
- * null,
- * null,
- * {
- * "sContentPadding": "mmm"
- * }
- * ]
- * } );
- * } );
- */
- "sContentPadding": "",
-
-
- /**
- * Allows a default value to be given for a column's data, and will be used
- * whenever a null data source is encountered (this can be because mDataProp
- * is set to null, or because the data source itself is null).
- * @type string
- * @default null
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * {
- * "mDataProp": null,
- * "sDefaultContent": "Edit",
- * "aTargets": [ -1 ]
- * }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * null,
- * null,
- * null,
- * {
- * "mDataProp": null,
- * "sDefaultContent": "Edit"
- * }
- * ]
- * } );
- * } );
- */
- "sDefaultContent": null,
-
-
- /**
- * This parameter is only used in DataTables' server-side processing. It can
- * be exceptionally useful to know what columns are being displayed on the
- * client side, and to map these to database fields. When defined, the names
- * also allow DataTables to reorder information from the server if it comes
- * back in an unexpected order (i.e. if you switch your columns around on the
- * client-side, your server-side code does not also need updating).
- * @type string
- * @default Empty string
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "sName": "engine", "aTargets": [ 0 ] },
- * { "sName": "browser", "aTargets": [ 1 ] },
- * { "sName": "platform", "aTargets": [ 2 ] },
- * { "sName": "version", "aTargets": [ 3 ] },
- * { "sName": "grade", "aTargets": [ 4 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "sName": "engine" },
- * { "sName": "browser" },
- * { "sName": "platform" },
- * { "sName": "version" },
- * { "sName": "grade" }
- * ]
- * } );
- * } );
- */
- "sName": "",
-
-
- /**
- * Defines a data source type for the sorting which can be used to read
- * realtime information from the table (updating the internally cached
- * version) prior to sorting. This allows sorting to occur on user editable
- * elements such as form inputs.
- * @type string
- * @default std
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "sSortDataType": "dom-text", "aTargets": [ 2, 3 ] },
- * { "sType": "numeric", "aTargets": [ 3 ] },
- * { "sSortDataType": "dom-select", "aTargets": [ 4 ] },
- * { "sSortDataType": "dom-checkbox", "aTargets": [ 5 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * null,
- * null,
- * { "sSortDataType": "dom-text" },
- * { "sSortDataType": "dom-text", "sType": "numeric" },
- * { "sSortDataType": "dom-select" },
- * { "sSortDataType": "dom-checkbox" }
- * ]
- * } );
- * } );
- */
- "sSortDataType": "std",
-
-
- /**
- * The title of this column.
- * @type string
- * @default null Derived from the 'TH' value for this column in the
- * original HTML table.
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "sTitle": "My column title", "aTargets": [ 0 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "sTitle": "My column title" },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "sTitle": null,
-
-
- /**
- * The type allows you to specify how the data for this column will be sorted.
- * Four types (string, numeric, date and html (which will strip HTML tags
- * before sorting)) are currently available. Note that only date formats
- * understood by Javascript's Date() object will be accepted as type date. For
- * example: "Mar 26, 2008 5:03 PM". May take the values: 'string', 'numeric',
- * 'date' or 'html' (by default). Further types can be adding through
- * plug-ins.
- * @type string
- * @default null Auto-detected from raw data
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "sType": "html", "aTargets": [ 0 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "sType": "html" },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "sType": null,
-
-
- /**
- * Defining the width of the column, this parameter may take any CSS value
- * (3em, 20px etc). DataTables applys 'smart' widths to columns which have not
- * been given a specific width through this interface ensuring that the table
- * remains readable.
- * @type string
- * @default null Automatic
- * @dtopt Columns
- *
- * @example
- * // Using aoColumnDefs
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumnDefs": [
- * { "sWidth": "20%", "aTargets": [ 0 ] }
- * ]
- * } );
- * } );
- *
- * @example
- * // Using aoColumns
- * $(document).ready(function() {
- * $('#example').dataTable( {
- * "aoColumns": [
- * { "sWidth": "20%" },
- * null,
- * null,
- * null,
- * null
- * ]
- * } );
- * } );
- */
- "sWidth": null
- };
-
-
-
- /**
- * DataTables settings object - this holds all the information needed for a
- * given table, including configuration, data and current application of the
- * table options. DataTables does not have a single instance for each DataTable
- * with the settings attached to that instance, but rather instances of the
- * DataTable "class" are created on-the-fly as needed (typically by a
- * $().dataTable() call) and the settings object is then applied to that
- * instance.
- *
- * Note that this object is related to {@link DataTable.defaults} but this
- * one is the internal data store for DataTables's cache of columns. It should
- * NOT be manipulated outside of DataTables. Any configuration should be done
- * through the initialisation options.
- * @namespace
- * @todo Really should attach the settings object to individual instances so we
- * don't need to create new instances on each $().dataTable() call (if the
- * table already exists). It would also save passing oSettings around and
- * into every single function. However, this is a very significant
- * architecture change for DataTables and will almost certainly break
- * backwards compatibility with older installations. This is something that
- * will be done in 2.0.
- */
- DataTable.models.oSettings = {
- /**
- * Primary features of DataTables and their enablement state.
- * @namespace
- */
- "oFeatures": {
-
- /**
- * Flag to say if DataTables should automatically try to calculate the
- * optimum table and columns widths (true) or not (false).
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bAutoWidth": null,
-
- /**
- * Delay the creation of TR and TD elements until they are actually
- * needed by a driven page draw. This can give a significant speed
- * increase for Ajax source and Javascript source data, but makes no
- * difference at all fro DOM and server-side processing tables.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bDeferRender": null,
-
- /**
- * Enable filtering on the table or not. Note that if this is disabled
- * then there is no filtering at all on the table, including fnFilter.
- * To just remove the filtering input use sDom and remove the 'f' option.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bFilter": null,
-
- /**
- * Table information element (the 'Showing x of y records' div) enable
- * flag.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bInfo": null,
-
- /**
- * Present a user control allowing the end user to change the page size
- * when pagination is enabled.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bLengthChange": null,
-
- /**
- * Pagination enabled or not. Note that if this is disabled then length
- * changing must also be disabled.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bPaginate": null,
-
- /**
- * Processing indicator enable flag whenever DataTables is enacting a
- * user request - typically an Ajax request for server-side processing.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bProcessing": null,
-
- /**
- * Server-side processing enabled flag - when enabled DataTables will
- * get all data from the server for every draw - there is no filtering,
- * sorting or paging done on the client-side.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bServerSide": null,
-
- /**
- * Sorting enablement flag.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bSort": null,
-
- /**
- * Apply a class to the columns which are being sorted to provide a
- * visual highlight or not. This can slow things down when enabled since
- * there is a lot of DOM interaction.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bSortClasses": null,
-
- /**
- * State saving enablement flag.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bStateSave": null
- },
-
-
- /**
- * Scrolling settings for a table.
- * @namespace
- */
- "oScroll": {
- /**
- * Indicate if DataTables should be allowed to set the padding / margin
- * etc for the scrolling header elements or not. Typically you will want
- * this.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bAutoCss": null,
-
- /**
- * When the table is shorter in height than sScrollY, collapse the
- * table container down to the height of the table (when true).
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bCollapse": null,
-
- /**
- * Infinite scrolling enablement flag. Now deprecated in favour of
- * using the Scroller plug-in.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bInfinite": null,
-
- /**
- * Width of the scrollbar for the web-browser's platform. Calculated
- * during table initialisation.
- * @type int
- * @default 0
- */
- "iBarWidth": 0,
-
- /**
- * Space (in pixels) between the bottom of the scrolling container and
- * the bottom of the scrolling viewport before the next page is loaded
- * when using infinite scrolling.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type int
- */
- "iLoadGap": null,
-
- /**
- * Viewport width for horizontal scrolling. Horizontal scrolling is
- * disabled if an empty string.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- */
- "sX": null,
-
- /**
- * Width to expand the table to when using x-scrolling. Typically you
- * should not need to use this.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- * @deprecated
- */
- "sXInner": null,
-
- /**
- * Viewport height for vertical scrolling. Vertical scrolling is disabled
- * if an empty string.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- */
- "sY": null
- },
-
- /**
- * Language information for the table.
- * @namespace
- * @extends DataTable.defaults.oLanguage
- */
- "oLanguage": {
- /**
- * Information callback function. See
- * {@link DataTable.defaults.fnInfoCallback}
- * @type function
- * @default
- */
- "fnInfoCallback": null
- },
-
- /**
- * Array referencing the nodes which are used for the features. The
- * parameters of this object match what is allowed by sDom - i.e.
- *
- *
'l' - Length changing
- *
'f' - Filtering input
- *
't' - The table!
- *
'i' - Information
- *
'p' - Pagination
- *
'r' - pRocessing
- *
- * @type array
- * @default []
- */
- "aanFeatures": [],
-
- /**
- * Store data information - see {@link DataTable.models.oRow} for detailed
- * information.
- * @type array
- * @default []
- */
- "aoData": [],
-
- /**
- * Array of indexes which are in the current display (after filtering etc)
- * @type array
- * @default []
- */
- "aiDisplay": [],
-
- /**
- * Array of indexes for display - no filtering
- * @type array
- * @default []
- */
- "aiDisplayMaster": [],
-
- /**
- * Store information about each column that is in use
- * @type array
- * @default []
- */
- "aoColumns": [],
-
- /**
- * Store information about the table's header
- * @type array
- * @default []
- */
- "aoHeader": [],
-
- /**
- * Store information about the table's footer
- * @type array
- * @default []
- */
- "aoFooter": [],
-
- /**
- * Search data array for regular expression searching
- * @type array
- * @default []
- */
- "asDataSearch": [],
-
- /**
- * Store the applied global search information in case we want to force a
- * research or compare the old search to a new one.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @namespace
- * @extends DataTable.models.oSearch
- */
- "oPreviousSearch": {},
-
- /**
- * Store the applied search for each column - see
- * {@link DataTable.models.oSearch} for the format that is used for the
- * filtering information for each column.
- * @type array
- * @default []
- */
- "aoPreSearchCols": [],
-
- /**
- * Sorting that is applied to the table. Note that the inner arrays are
- * used in the following manner:
- *
- *
Index 0 - column number
- *
Index 1 - current sorting direction
- *
Index 2 - index of asSorting for this column
- *
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type array
- * @todo These inner arrays should really be objects
- */
- "aaSorting": null,
-
- /**
- * Sorting that is always applied to the table (i.e. prefixed in front of
- * aaSorting).
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type array|null
- * @default null
- */
- "aaSortingFixed": null,
-
- /**
- * Classes to use for the striping of a table.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type array
- * @default []
- */
- "asStripeClasses": null,
-
- /**
- * If restoring a table - we should restore its striping classes as well
- * @type array
- * @default []
- */
- "asDestroyStripes": [],
-
- /**
- * If restoring a table - we should restore its width
- * @type int
- * @default 0
- */
- "sDestroyWidth": 0,
-
- /**
- * Callback functions array for every time a row is inserted (i.e. on a draw).
- * @type array
- * @default []
- */
- "aoRowCallback": [],
-
- /**
- * Callback functions for the header on each draw.
- * @type array
- * @default []
- */
- "aoHeaderCallback": [],
-
- /**
- * Callback function for the footer on each draw.
- * @type array
- * @default []
- */
- "aoFooterCallback": [],
-
- /**
- * Array of callback functions for draw callback functions
- * @type array
- * @default []
- */
- "aoDrawCallback": [],
-
- /**
- * Array of callback functions for row created function
- * @type array
- * @default []
- */
- "aoRowCreatedCallback": [],
-
- /**
- * Callback functions for just before the table is redrawn. A return of
- * false will be used to cancel the draw.
- * @type array
- * @default []
- */
- "aoPreDrawCallback": [],
-
- /**
- * Callback functions for when the table has been initialised.
- * @type array
- * @default []
- */
- "aoInitComplete": [],
-
-
- /**
- * Callbacks for modifying the settings to be stored for state saving, prior to
- * saving state.
- * @type array
- * @default []
- */
- "aoStateSaveParams": [],
-
- /**
- * Callbacks for modifying the settings that have been stored for state saving
- * prior to using the stored values to restore the state.
- * @type array
- * @default []
- */
- "aoStateLoadParams": [],
-
- /**
- * Callbacks for operating on the settings object once the saved state has been
- * loaded
- * @type array
- * @default []
- */
- "aoStateLoaded": [],
-
- /**
- * Cache the table ID for quick access
- * @type string
- * @default Empty string
- */
- "sTableId": "",
-
- /**
- * The TABLE node for the main table
- * @type node
- * @default null
- */
- "nTable": null,
-
- /**
- * Permanent ref to the thead element
- * @type node
- * @default null
- */
- "nTHead": null,
-
- /**
- * Permanent ref to the tfoot element - if it exists
- * @type node
- * @default null
- */
- "nTFoot": null,
-
- /**
- * Permanent ref to the tbody element
- * @type node
- * @default null
- */
- "nTBody": null,
-
- /**
- * Cache the wrapper node (contains all DataTables controlled elements)
- * @type node
- * @default null
- */
- "nTableWrapper": null,
-
- /**
- * Indicate if when using server-side processing the loading of data
- * should be deferred until the second draw.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- * @default false
- */
- "bDeferLoading": false,
-
- /**
- * Indicate if all required information has been read in
- * @type boolean
- * @default false
- */
- "bInitialised": false,
-
- /**
- * Information about open rows. Each object in the array has the parameters
- * 'nTr' and 'nParent'
- * @type array
- * @default []
- */
- "aoOpenRows": [],
-
- /**
- * Dictate the positioning of DataTables' control elements - see
- * {@link DataTable.model.oInit.sDom}.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- * @default null
- */
- "sDom": null,
-
- /**
- * Which type of pagination should be used.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- * @default two_button
- */
- "sPaginationType": "two_button",
-
- /**
- * The cookie duration (for bStateSave) in seconds.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type int
- * @default 0
- */
- "iCookieDuration": 0,
-
- /**
- * The cookie name prefix.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- * @default Empty string
- */
- "sCookiePrefix": "",
-
- /**
- * Callback function for cookie creation.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type function
- * @default null
- */
- "fnCookieCallback": null,
-
- /**
- * Array of callback functions for state saving. Each array element is an
- * object with the following parameters:
- *
- *
function:fn - function to call. Takes two parameters, oSettings
- * and the JSON string to save that has been thus far created. Returns
- * a JSON string to be inserted into a json object
- * (i.e. '"param": [ 0, 1, 2]')
- *
string:sName - name of callback
- *
- * @type array
- * @default []
- */
- "aoStateSave": [],
-
- /**
- * Array of callback functions for state loading. Each array element is an
- * object with the following parameters:
- *
- *
function:fn - function to call. Takes two parameters, oSettings
- * and the object stored. May return false to cancel state loading
- *
string:sName - name of callback
- *
- * @type array
- * @default []
- */
- "aoStateLoad": [],
-
- /**
- * State that was loaded from the cookie. Useful for back reference
- * @type object
- * @default null
- */
- "oLoadedState": null,
-
- /**
- * Source url for AJAX data for the table.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- * @default null
- */
- "sAjaxSource": null,
-
- /**
- * Property from a given object from which to read the table data from. This
- * can be an empty string (when not server-side processing), in which case
- * it is assumed an an array is given directly.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- */
- "sAjaxDataProp": null,
-
- /**
- * Note if draw should be blocked while getting data
- * @type boolean
- * @default true
- */
- "bAjaxDataGet": true,
-
- /**
- * The last jQuery XHR object that was used for server-side data gathering.
- * This can be used for working with the XHR information in one of the
- * callbacks
- * @type object
- * @default null
- */
- "jqXHR": null,
-
- /**
- * Function to get the server-side data.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type function
- */
- "fnServerData": null,
-
- /**
- * Functions which are called prior to sending an Ajax request so extra
- * parameters can easily be sent to the server
- * @type array
- * @default []
- */
- "aoServerParams": [],
-
- /**
- * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if
- * required).
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type string
- */
- "sServerMethod": null,
-
- /**
- * Format numbers for display.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type function
- */
- "fnFormatNumber": null,
-
- /**
- * List of options that can be used for the user selectable length menu.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type array
- * @default []
- */
- "aLengthMenu": null,
-
- /**
- * Counter for the draws that the table does. Also used as a tracker for
- * server-side processing
- * @type int
- * @default 0
- */
- "iDraw": 0,
-
- /**
- * Indicate if a redraw is being done - useful for Ajax
- * @type boolean
- * @default false
- */
- "bDrawing": false,
-
- /**
- * Draw index (iDraw) of the last error when parsing the returned data
- * @type int
- * @default -1
- */
- "iDrawError": -1,
-
- /**
- * Paging display length
- * @type int
- * @default 10
- */
- "_iDisplayLength": 10,
-
- /**
- * Paging start point - aiDisplay index
- * @type int
- * @default 0
- */
- "_iDisplayStart": 0,
-
- /**
- * Paging end point - aiDisplay index. Use fnDisplayEnd rather than
- * this property to get the end point
- * @type int
- * @default 10
- * @private
- */
- "_iDisplayEnd": 10,
-
- /**
- * Server-side processing - number of records in the result set
- * (i.e. before filtering), Use fnRecordsTotal rather than
- * this property to get the value of the number of records, regardless of
- * the server-side processing setting.
- * @type int
- * @default 0
- * @private
- */
- "_iRecordsTotal": 0,
-
- /**
- * Server-side processing - number of records in the current display set
- * (i.e. after filtering). Use fnRecordsDisplay rather than
- * this property to get the value of the number of records, regardless of
- * the server-side processing setting.
- * @type boolean
- * @default 0
- * @private
- */
- "_iRecordsDisplay": 0,
-
- /**
- * Flag to indicate if jQuery UI marking and classes should be used.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bJUI": null,
-
- /**
- * The classes to use for the table
- * @type object
- * @default {}
- */
- "oClasses": {},
-
- /**
- * Flag attached to the settings object so you can check in the draw
- * callback if filtering has been done in the draw. Deprecated in favour of
- * events.
- * @type boolean
- * @default false
- * @deprecated
- */
- "bFiltered": false,
-
- /**
- * Flag attached to the settings object so you can check in the draw
- * callback if sorting has been done in the draw. Deprecated in favour of
- * events.
- * @type boolean
- * @default false
- * @deprecated
- */
- "bSorted": false,
-
- /**
- * Indicate that if multiple rows are in the header and there is more than
- * one unique cell per column, if the top one (true) or bottom one (false)
- * should be used for sorting / title by DataTables.
- * Note that this parameter will be set by the initialisation routine. To
- * set a default use {@link DataTable.defaults}.
- * @type boolean
- */
- "bSortCellsTop": null,
-
- /**
- * Initialisation object that is used for the table
- * @type object
- * @default null
- */
- "oInit": null,
-
- /**
- * Destroy callback functions - for plug-ins to attach themselves to the
- * destroy so they can clean up markup and events.
- * @type array
- * @default []
- */
- "aoDestroyCallback": [],
-
-
- /**
- * Get the number of records in the current record set, before filtering
- * @type function
- */
- "fnRecordsTotal": function ()
- {
- if ( this.oFeatures.bServerSide ) {
- return parseInt(this._iRecordsTotal, 10);
- } else {
- return this.aiDisplayMaster.length;
- }
- },
-
- /**
- * Get the number of records in the current record set, after filtering
- * @type function
- */
- "fnRecordsDisplay": function ()
- {
- if ( this.oFeatures.bServerSide ) {
- return parseInt(this._iRecordsDisplay, 10);
- } else {
- return this.aiDisplay.length;
- }
- },
-
- /**
- * Set the display end point - aiDisplay index
- * @type function
- * @todo Should do away with _iDisplayEnd and calculate it on-the-fly here
- */
- "fnDisplayEnd": function ()
- {
- if ( this.oFeatures.bServerSide ) {
- if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) {
- return this._iDisplayStart+this.aiDisplay.length;
- } else {
- return Math.min( this._iDisplayStart+this._iDisplayLength,
- this._iRecordsDisplay );
- }
- } else {
- return this._iDisplayEnd;
- }
- },
-
- /**
- * The DataTables object for this table
- * @type object
- * @default null
- */
- "oInstance": null,
-
- /**
- * Unique identifier for each instance of the DataTables object. If there
- * is an ID on the table node, then it takes that value, otherwise an
- * incrementing internal counter is used.
- * @type string
- * @default null
- */
- "sInstance": null,
-
- /**
- * tabindex attribute value that is added to DataTables control elements, allowing
- * keyboard navigation of the table and its controls.
- */
- "iTabIndex": 0,
-
- /**
- * DIV container for the footer scrolling table if scrolling
- */
- "nScrollHead": null,
-
- /**
- * DIV container for the footer scrolling table if scrolling
- */
- "nScrollFoot": null
- };
-
- /**
- * Extension object for DataTables that is used to provide all extension options.
- *
- * Note that the DataTable.ext object is available through
- * jQuery.fn.dataTable.ext where it may be accessed and manipulated. It is
- * also aliased to jQuery.fn.dataTableExt for historic reasons.
- * @namespace
- * @extends DataTable.models.ext
- */
- DataTable.ext = $.extend( true, {}, DataTable.models.ext );
-
- $.extend( DataTable.ext.oStdClasses, {
- "sTable": "dataTable",
-
- /* Two buttons buttons */
- "sPagePrevEnabled": "paginate_enabled_previous",
- "sPagePrevDisabled": "paginate_disabled_previous",
- "sPageNextEnabled": "paginate_enabled_next",
- "sPageNextDisabled": "paginate_disabled_next",
- "sPageJUINext": "",
- "sPageJUIPrev": "",
-
- /* Full numbers paging buttons */
- "sPageButton": "paginate_button",
- "sPageButtonActive": "paginate_active",
- "sPageButtonStaticDisabled": "paginate_button paginate_button_disabled",
- "sPageFirst": "first",
- "sPagePrevious": "previous",
- "sPageNext": "next",
- "sPageLast": "last",
-
- /* Striping classes */
- "sStripeOdd": "odd",
- "sStripeEven": "even",
-
- /* Empty row */
- "sRowEmpty": "dataTables_empty",
-
- /* Features */
- "sWrapper": "dataTables_wrapper",
- "sFilter": "dataTables_filter",
- "sInfo": "dataTables_info",
- "sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
- "sLength": "dataTables_length",
- "sProcessing": "dataTables_processing",
-
- /* Sorting */
- "sSortAsc": "sorting_asc",
- "sSortDesc": "sorting_desc",
- "sSortable": "sorting", /* Sortable in both directions */
- "sSortableAsc": "sorting_asc_disabled",
- "sSortableDesc": "sorting_desc_disabled",
- "sSortableNone": "sorting_disabled",
- "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
- "sSortJUIAsc": "",
- "sSortJUIDesc": "",
- "sSortJUI": "",
- "sSortJUIAscAllowed": "",
- "sSortJUIDescAllowed": "",
- "sSortJUIWrapper": "",
- "sSortIcon": "",
-
- /* Scrolling */
- "sScrollWrapper": "dataTables_scroll",
- "sScrollHead": "dataTables_scrollHead",
- "sScrollHeadInner": "dataTables_scrollHeadInner",
- "sScrollBody": "dataTables_scrollBody",
- "sScrollFoot": "dataTables_scrollFoot",
- "sScrollFootInner": "dataTables_scrollFootInner",
-
- /* Misc */
- "sFooterTH": ""
- } );
-
-
- $.extend( DataTable.ext.oJUIClasses, DataTable.ext.oStdClasses, {
- /* Two buttons buttons */
- "sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left",
- "sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",
- "sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right",
- "sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",
- "sPageJUINext": "ui-icon ui-icon-circle-arrow-e",
- "sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",
-
- /* Full numbers paging buttons */
- "sPageButton": "fg-button ui-button ui-state-default",
- "sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled",
- "sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled",
- "sPageFirst": "first ui-corner-tl ui-corner-bl",
- "sPageLast": "last ui-corner-tr ui-corner-br",
-
- /* Features */
- "sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+
- "ui-buttonset-multi paging_", /* Note that the type is postfixed */
-
- /* Sorting */
- "sSortAsc": "ui-state-default",
- "sSortDesc": "ui-state-default",
- "sSortable": "ui-state-default",
- "sSortableAsc": "ui-state-default",
- "sSortableDesc": "ui-state-default",
- "sSortableNone": "ui-state-default",
- "sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",
- "sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",
- "sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",
- "sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",
- "sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",
- "sSortJUIWrapper": "DataTables_sort_wrapper",
- "sSortIcon": "DataTables_sort_icon",
-
- /* Scrolling */
- "sScrollHead": "dataTables_scrollHead ui-state-default",
- "sScrollFoot": "dataTables_scrollFoot ui-state-default",
-
- /* Misc */
- "sFooterTH": "ui-state-default"
- } );
-
-
- /*
- * Variable: oPagination
- * Purpose:
- * Scope: jQuery.fn.dataTableExt
- */
- $.extend( DataTable.ext.oPagination, {
- /*
- * Variable: two_button
- * Purpose: Standard two button (forward/back) pagination
- * Scope: jQuery.fn.dataTableExt.oPagination
- */
- "two_button": {
- /*
- * Function: oPagination.two_button.fnInit
- * Purpose: Initialise dom elements required for pagination with forward/back buttons only
- * Returns: -
- * Inputs: object:oSettings - dataTables settings object
- * node:nPaging - the DIV which contains this pagination control
- * function:fnCallbackDraw - draw function which must be called on update
- */
- "fnInit": function ( oSettings, nPaging, fnCallbackDraw )
- {
- var oLang = oSettings.oLanguage.oPaginate;
- var oClasses = oSettings.oClasses;
- var fnClickHandler = function ( e ) {
- if ( oSettings.oApi._fnPageChange( oSettings, e.data.action ) )
- {
- fnCallbackDraw( oSettings );
- }
- };
-
- var sAppend = (!oSettings.bJUI) ?
- ''+oLang.sPrevious+''+
- ''+oLang.sNext+''
- :
- ''+
- '';
- $(nPaging).append( sAppend );
-
- var els = $('a', nPaging);
- var nPrevious = els[0],
- nNext = els[1];
-
- oSettings.oApi._fnBindAction( nPrevious, {action: "previous"}, fnClickHandler );
- oSettings.oApi._fnBindAction( nNext, {action: "next"}, fnClickHandler );
-
- /* ID the first elements only */
- if ( !oSettings.aanFeatures.p )
- {
- nPaging.id = oSettings.sTableId+'_paginate';
- nPrevious.id = oSettings.sTableId+'_previous';
- nNext.id = oSettings.sTableId+'_next';
-
- nPrevious.setAttribute('aria-controls', oSettings.sTableId);
- nNext.setAttribute('aria-controls', oSettings.sTableId);
- }
- },
-
- /*
- * Function: oPagination.two_button.fnUpdate
- * Purpose: Update the two button pagination at the end of the draw
- * Returns: -
- * Inputs: object:oSettings - dataTables settings object
- * function:fnCallbackDraw - draw function to call on page change
- */
- "fnUpdate": function ( oSettings, fnCallbackDraw )
- {
- if ( !oSettings.aanFeatures.p )
- {
- return;
- }
-
- var oClasses = oSettings.oClasses;
- var an = oSettings.aanFeatures.p;
-
- /* Loop over each instance of the pager */
- for ( var i=0, iLen=an.length ; i'+oLang.sFirst+''+
- ''+oLang.sPrevious+''+
- ''+
- ''+oLang.sNext+''+
- ''+oLang.sLast+''
- );
- var els = $('a', nPaging);
- var nFirst = els[0],
- nPrev = els[1],
- nNext = els[2],
- nLast = els[3];
-
- oSettings.oApi._fnBindAction( nFirst, {action: "first"}, fnClickHandler );
- oSettings.oApi._fnBindAction( nPrev, {action: "previous"}, fnClickHandler );
- oSettings.oApi._fnBindAction( nNext, {action: "next"}, fnClickHandler );
- oSettings.oApi._fnBindAction( nLast, {action: "last"}, fnClickHandler );
-
- /* ID the first elements only */
- if ( !oSettings.aanFeatures.p )
- {
- nPaging.id = oSettings.sTableId+'_paginate';
- nFirst.id =oSettings.sTableId+'_first';
- nPrev.id =oSettings.sTableId+'_previous';
- nNext.id =oSettings.sTableId+'_next';
- nLast.id =oSettings.sTableId+'_last';
- }
- },
-
- /*
- * Function: oPagination.full_numbers.fnUpdate
- * Purpose: Update the list of page buttons shows
- * Returns: -
- * Inputs: object:oSettings - dataTables settings object
- * function:fnCallbackDraw - draw function to call on page change
- */
- "fnUpdate": function ( oSettings, fnCallbackDraw )
- {
- if ( !oSettings.aanFeatures.p )
- {
- return;
- }
-
- var iPageCount = DataTable.ext.oPagination.iFullNumbersShowPages;
- var iPageCountHalf = Math.floor(iPageCount / 2);
- var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
- var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
- var sList = "";
- var iStartButton, iEndButton, i, iLen;
- var oClasses = oSettings.oClasses;
- var anButtons, anStatic, nPaginateList;
- var an = oSettings.aanFeatures.p;
- var fnBind = function (j) {
- oSettings.oApi._fnBindAction( this, {"page": j+iStartButton-1}, function(e) {
- /* Use the information in the element to jump to the required page */
- oSettings.oApi._fnPageChange( oSettings, e.data.page );
- fnCallbackDraw( oSettings );
- e.preventDefault();
- } );
- };
-
- /* Pages calculation */
- if ( oSettings._iDisplayLength === -1 )
- {
- iStartButton = 1;
- iEndButton = 1;
- iCurrentPage = 1;
- }
- else if (iPages < iPageCount)
- {
- iStartButton = 1;
- iEndButton = iPages;
- }
- else if (iCurrentPage <= iPageCountHalf)
- {
- iStartButton = 1;
- iEndButton = iPageCount;
- }
- else if (iCurrentPage >= (iPages - iPageCountHalf))
- {
- iStartButton = iPages - iPageCount + 1;
- iEndButton = iPages;
- }
- else
- {
- iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;
- iEndButton = iStartButton + iPageCount - 1;
- }
-
-
- /* Build the dynamic list */
- for ( i=iStartButton ; i<=iEndButton ; i++ )
- {
- sList += (iCurrentPage !== i) ?
- ''+oSettings.fnFormatNumber(i)+'' :
- ''+oSettings.fnFormatNumber(i)+'';
- }
-
- /* Loop over each instance of the pager */
- for ( i=0, iLen=an.length ; i y) ? 1 : 0));
- },
-
- "string-desc": function ( x, y )
- {
- return ((x < y) ? 1 : ((x > y) ? -1 : 0));
- },
-
-
- /*
- * html sorting (ignore html tags)
- */
- "html-pre": function ( a )
- {
- return a.replace( /<.*?>/g, "" ).toLowerCase();
- },
-
- "html-asc": function ( x, y )
- {
- return ((x < y) ? -1 : ((x > y) ? 1 : 0));
- },
-
- "html-desc": function ( x, y )
- {
- return ((x < y) ? 1 : ((x > y) ? -1 : 0));
- },
-
-
- /*
- * date sorting
- */
- "date-pre": function ( a )
- {
- var x = Date.parse( a );
-
- if ( isNaN(x) || x==="" )
- {
- x = Date.parse( "01/01/1970 00:00:00" );
- }
- return x;
- },
-
- "date-asc": function ( x, y )
- {
- return x - y;
- },
-
- "date-desc": function ( x, y )
- {
- return y - x;
- },
-
-
- /*
- * numerical sorting
- */
- "numeric-pre": function ( a )
- {
- return (a=="-" || a==="") ? 0 : a*1;
- },
-
- "numeric-asc": function ( x, y )
- {
- return x - y;
- },
-
- "numeric-desc": function ( x, y )
- {
- return y - x;
- }
- } );
-
-
- $.extend( DataTable.ext.aTypes, [
- /*
- * Function: -
- * Purpose: Check to see if a string is numeric
- * Returns: string:'numeric' or null
- * Inputs: mixed:sText - string to check
- */
- function ( sData )
- {
- /* Allow zero length strings as a number */
- if ( typeof sData === 'number' )
- {
- return 'numeric';
- }
- else if ( typeof sData !== 'string' )
- {
- return null;
- }
-
- var sValidFirstChars = "0123456789-";
- var sValidChars = "0123456789.";
- var Char;
- var bDecimal = false;
-
- /* Check for a valid first char (no period and allow negatives) */
- Char = sData.charAt(0);
- if (sValidFirstChars.indexOf(Char) == -1)
- {
- return null;
- }
-
- /* Check all the other characters are valid */
- for ( var i=1 ; i') != -1 )
- {
- return 'html';
- }
- return null;
- }
- ] );
-
-
- // jQuery aliases
- $.fn.DataTable = DataTable;
- $.fn.dataTable = DataTable;
- $.fn.dataTableSettings = DataTable.settings;
- $.fn.dataTableExt = DataTable.ext;
-
-
- // Information about events fired by DataTables - for documentation.
- /**
- * Draw event, fired whenever the table is redrawn on the page, at the same point as
- * fnDrawCallback. This may be useful for binding events or performing calculations when
- * the table is altered at all.
- * @name DataTable#draw
- * @event
- * @param {event} e jQuery event object
- * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
- */
-
- /**
- * Filter event, fired when the filtering applied to the table (using the build in global
- * global filter, or column filters) is altered.
- * @name DataTable#filter
- * @event
- * @param {event} e jQuery event object
- * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
- */
-
- /**
- * Page change event, fired when the paging of the table is altered.
- * @name DataTable#page
- * @event
- * @param {event} e jQuery event object
- * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
- */
-
- /**
- * Sort event, fired when the sorting applied to the table is altered.
- * @name DataTable#sort
- * @event
- * @param {event} e jQuery event object
- * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
- */
-
- /**
- * DataTables initialisation complete event, fired when the table is fully drawn,
- * including Ajax data loaded, if Ajax data is required.
- * @name DataTable#init
- * @event
- * @param {event} e jQuery event object
- * @param {object} oSettings DataTables settings object
- * @param {object} json The JSON object request from the server - only
- * present if client-side Ajax sourced data is used
- */
-
- /**
- * State save event, fired when the table has changed state a new state save is required.
- * This method allows modification of the state saving object prior to actually doing the
- * save, including addition or other state properties (for plug-ins) or modification
- * of a DataTables core property.
- * @name DataTable#stateSaveParams
- * @event
- * @param {event} e jQuery event object
- * @param {object} oSettings DataTables settings object
- * @param {object} json The state information to be saved
- */
-
- /**
- * State load event, fired when the table is loading state from the stored data, but
- * prior to the settings object being modified by the saved state - allowing modification
- * of the saved state is required or loading of state for a plug-in.
- * @name DataTable#stateLoadParams
- * @event
- * @param {event} e jQuery event object
- * @param {object} oSettings DataTables settings object
- * @param {object} json The saved state information
- */
-
- /**
- * State loaded event, fired when state has been loaded from stored data and the settings
- * object has been modified by the loaded data.
- * @name DataTable#stateLoaded
- * @event
- * @param {event} e jQuery event object
- * @param {object} oSettings DataTables settings object
- * @param {object} json The saved state information
- */
-
- /**
- * Processing event, fired when DataTables is doing some kind of processing (be it,
- * sort, filter or anything else). Can be used to indicate to the end user that
- * there is something happening, or that something has finished.
- * @name DataTable#processing
- * @event
- * @param {event} e jQuery event object
- * @param {object} oSettings DataTables settings object
- * @param {boolean} bShow Flag for if DataTables is doing processing or not
- */
-
- /**
- * Ajax (XHR) event, fired whenever an Ajax request is completed from a request to
- * made to the server for new data (note that this trigger is called in fnServerData,
- * if you override fnServerData and which to use this event, you need to trigger it in
- * you success function).
- * @name DataTable#xhr
- * @event
- * @param {event} e jQuery event object
- * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
- */
-
- /**
- * Destroy event, fired when the DataTable is destroyed by calling fnDestroy or passing
- * the bDestroy:true parameter in the initialisation object. This can be used to remove
- * bound events, added DOM nodes, etc.
- * @name DataTable#destroy
- * @event
- * @param {event} e jQuery event object
- * @param {object} o DataTables settings object {@link DataTable.models.oSettings}
- */
-}(jQuery, window, document, undefined));
+(function(X,l,n){var L=function(h){var j=function(e){function o(a,b){var c=j.defaults.columns,d=a.aoColumns.length,c=h.extend({},j.models.oColumn,c,{sSortingClass:a.oClasses.sSortable,sSortingClassJUI:a.oClasses.sSortJUI,nTh:b?b:l.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.oDefaults:d});a.aoColumns.push(c);if(a.aoPreSearchCols[d]===n||null===a.aoPreSearchCols[d])a.aoPreSearchCols[d]=h.extend({},j.models.oSearch);else if(c=a.aoPreSearchCols[d],
+c.bRegex===n&&(c.bRegex=!0),c.bSmart===n&&(c.bSmart=!0),c.bCaseInsensitive===n)c.bCaseInsensitive=!0;m(a,d,null)}function m(a,b,c){var d=a.aoColumns[b];c!==n&&null!==c&&(c.mDataProp&&!c.mData&&(c.mData=c.mDataProp),c.sType!==n&&(d.sType=c.sType,d._bAutoType=!1),h.extend(d,c),p(d,c,"sWidth","sWidthOrig"),c.iDataSort!==n&&(d.aDataSort=[c.iDataSort]),p(d,c,"aDataSort"));var i=d.mRender?Q(d.mRender):null,f=Q(d.mData);d.fnGetData=function(a,b){var c=f(a,b);return d.mRender&&b&&""!==b?i(c,b,a):c};d.fnSetData=
+L(d.mData);a.oFeatures.bSort||(d.bSortable=!1);!d.bSortable||-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableNone,d.sSortingClassJUI=""):-1==h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortable,d.sSortingClassJUI=a.oClasses.sSortJUI):-1!=h.inArray("asc",d.asSorting)&&-1==h.inArray("desc",d.asSorting)?(d.sSortingClass=a.oClasses.sSortableAsc,d.sSortingClassJUI=a.oClasses.sSortJUIAscAllowed):-1==
+h.inArray("asc",d.asSorting)&&-1!=h.inArray("desc",d.asSorting)&&(d.sSortingClass=a.oClasses.sSortableDesc,d.sSortingClassJUI=a.oClasses.sSortJUIDescAllowed)}function k(a){if(!1===a.oFeatures.bAutoWidth)return!1;da(a);for(var b=0,c=a.aoColumns.length;bj[f])d(a.aoColumns.length+j[f],b[i]);else if("string"===typeof j[f]){e=0;for(w=a.aoColumns.length;eb&&a[d]--; -1!=c&&a.splice(c,1)}function S(a,b,c){var d=a.aoColumns[c];return d.fnRender({iDataRow:b,iDataColumn:c,oSettings:a,aData:a.aoData[b]._aData,mDataProp:d.mData},v(a,b,c,"display"))}function ea(a,b){var c=a.aoData[b],d;if(null===c.nTr){c.nTr=l.createElement("tr");c.nTr._DT_RowIndex=b;c._aData.DT_RowId&&(c.nTr.id=c._aData.DT_RowId);c._aData.DT_RowClass&&
+(c.nTr.className=c._aData.DT_RowClass);for(var i=0,f=a.aoColumns.length;i=a.fnRecordsDisplay()?0:a.iInitDisplayStart,a.iInitDisplayStart=-1,y(a));if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++;else if(a.oFeatures.bServerSide){if(!a.bDestroying&&!wa(a))return}else a.iDraw++;if(0!==a.aiDisplay.length){var g=
+a._iDisplayStart;d=a._iDisplayEnd;a.oFeatures.bServerSide&&(g=0,d=a.aoData.length);for(;g
")[0];w=d[m+
+1];if("'"==w||'"'==w){o="";for(k=2;d[m+k]!=w;)o+=d[m+k],k++;"H"==o?o=a.oClasses.sJUIHeader:"F"==o&&(o=a.oClasses.sJUIFooter);-1!=o.indexOf(".")?(w=o.split("."),e.id=w[0].substr(1,w[0].length-1),e.className=w[1]):"#"==o.charAt(0)?e.id=o.substr(1,o.length-1):e.className=o;m+=k}c.appendChild(e);c=e}else if(">"==g)c=c.parentNode;else if("l"==g&&a.oFeatures.bPaginate&&a.oFeatures.bLengthChange)i=ya(a),f=1;else if("f"==g&&a.oFeatures.bFilter)i=za(a),f=1;else if("r"==g&&a.oFeatures.bProcessing)i=Aa(a),f=
+1;else if("t"==g)i=Ba(a),f=1;else if("i"==g&&a.oFeatures.bInfo)i=Ca(a),f=1;else if("p"==g&&a.oFeatures.bPaginate)i=Da(a),f=1;else if(0!==j.ext.aoFeatures.length){e=j.ext.aoFeatures;k=0;for(w=e.length;k'):""===c?'':c+' ',d=l.createElement("div");d.className=a.oClasses.sFilter;d.innerHTML="";a.aanFeatures.f||(d.id=a.sTableId+"_filter");c=h('input[type="text"]',d);d._DT_Input=c[0];c.val(b.sSearch.replace('"',"""));c.bind("keyup.DT",function(){for(var c=a.aanFeatures.f,d=this.value===""?"":this.value,
+g=0,e=c.length;g=b.length)a.aiDisplay.splice(0,a.aiDisplay.length),a.aiDisplay=a.aiDisplayMaster.slice();else if(a.aiDisplay.length==a.aiDisplayMaster.length||i.sSearch.length>b.length||1==c||0!==b.indexOf(i.sSearch)){a.aiDisplay.splice(0,
+a.aiDisplay.length);la(a,1);for(b=0;b").html(c).text());
+return c.replace(/[\n\r]/g," ")}function ma(a,b,c,d){if(c)return a=b?a.split(" "):oa(a).split(" "),a="^(?=.*?"+a.join(")(?=.*?")+").*$",RegExp(a,d?"i":"");a=b?a:oa(a);return RegExp(a,d?"i":"")}function Ja(a,b){return"function"===typeof j.ext.ofnSearch[b]?j.ext.ofnSearch[b](a):null===a?"":"html"==b?a.replace(/[\r\n]/g," ").replace(/<.*?>/g,""):"string"===typeof a?a.replace(/[\r\n]/g," "):a}function oa(a){return a.replace(RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),
+"\\$1")}function Ca(a){var b=l.createElement("div");b.className=a.oClasses.sInfo;a.aanFeatures.i||(a.aoDrawCallback.push({fn:Ka,sName:"information"}),b.id=a.sTableId+"_info");a.nTable.setAttribute("aria-describedby",a.sTableId+"_info");return b}function Ka(a){if(a.oFeatures.bInfo&&0!==a.aanFeatures.i.length){var b=a.oLanguage,c=a._iDisplayStart+1,d=a.fnDisplayEnd(),i=a.fnRecordsTotal(),f=a.fnRecordsDisplay(),g;g=0===f?b.sInfoEmpty:b.sInfo;f!=i&&(g+=" "+b.sInfoFiltered);g+=b.sInfoPostFix;g=ja(a,g);
+null!==b.fnInfoCallback&&(g=b.fnInfoCallback.call(a.oInstance,a,c,d,i,f,g));a=a.aanFeatures.i;b=0;for(c=a.length;b",c,d,i=a.aLengthMenu;if(2==i.length&&"object"===typeof i[0]&&"object"===typeof i[1]){c=0;for(d=i[0].length;c'+i[1][c]+""}else{c=0;for(d=i.length;c'+i[c]+""}b+="";i=l.createElement("div");a.aanFeatures.l||
+(i.id=a.sTableId+"_length");i.className=a.oClasses.sLength;i.innerHTML="";h('select option[value="'+a._iDisplayLength+'"]',i).attr("selected",!0);h("select",i).bind("change.DT",function(){var b=h(this).val(),i=a.aanFeatures.l;c=0;for(d=i.length;ca.aiDisplay.length||-1==a._iDisplayLength?a.aiDisplay.length:a._iDisplayStart+a._iDisplayLength}function Da(a){if(a.oScroll.bInfinite)return null;var b=l.createElement("div");b.className=a.oClasses.sPaging+a.sPaginationType;j.ext.oPagination[a.sPaginationType].fnInit(a,
+b,function(a){y(a);x(a)});a.aanFeatures.p||a.aoDrawCallback.push({fn:function(a){j.ext.oPagination[a.sPaginationType].fnUpdate(a,function(a){y(a);x(a)})},sName:"pagination"});return b}function qa(a,b){var c=a._iDisplayStart;if("number"===typeof b)a._iDisplayStart=b*a._iDisplayLength,a._iDisplayStart>a.fnRecordsDisplay()&&(a._iDisplayStart=0);else if("first"==b)a._iDisplayStart=0;else if("previous"==b)a._iDisplayStart=0<=a._iDisplayLength?a._iDisplayStart-a._iDisplayLength:0,0>a._iDisplayStart&&(a._iDisplayStart=
+0);else if("next"==b)0<=a._iDisplayLength?a._iDisplayStart+a._iDisplayLengthh(a.nTable).height()-a.oScroll.iLoadGap&&a.fnDisplayEnd()d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(h(a.nTable).outerWidth()-a.oScroll.iBarWidth)}else""!==a.oScroll.sXInner?a.nTable.style.width=
+q(a.oScroll.sXInner):i==h(d).width()&&h(d).height()i-a.oScroll.iBarWidth&&(a.nTable.style.width=q(i))):a.nTable.style.width=q(i);i=h(a.nTable).outerWidth();C(s,e);C(function(a){p.push(q(h(a).width()))},e);C(function(a,b){a.style.width=p[b]},g);h(e).height(0);null!==a.nTFoot&&(C(s,j),C(function(a){n.push(q(h(a).width()))},j),C(function(a,b){a.style.width=n[b]},o),h(j).height(0));C(function(a,b){a.innerHTML=
+"";a.style.width=p[b]},e);null!==a.nTFoot&&C(function(a,b){a.innerHTML="";a.style.width=n[b]},j);if(h(a.nTable).outerWidth()d.offsetHeight||"scroll"==h(d).css("overflow-y")?i+a.oScroll.iBarWidth:i;if(r&&(d.scrollHeight>d.offsetHeight||"scroll"==h(d).css("overflow-y")))a.nTable.style.width=q(g-a.oScroll.iBarWidth);d.style.width=q(g);a.nScrollHead.style.width=q(g);null!==a.nTFoot&&(a.nScrollFoot.style.width=q(g));""===a.oScroll.sX?D(a,1,"The table cannot fit into the current element which will cause column misalignment. The table has been drawn at its minimum possible width."):
+""!==a.oScroll.sXInner&&D(a,1,"The table cannot fit into the current element which will cause column misalignment. Increase the sScrollXInner value or remove it to allow automatic calculation")}else d.style.width=q("100%"),a.nScrollHead.style.width=q("100%"),null!==a.nTFoot&&(a.nScrollFoot.style.width=q("100%"));""===a.oScroll.sY&&r&&(d.style.height=q(a.nTable.offsetHeight+a.oScroll.iBarWidth));""!==a.oScroll.sY&&a.oScroll.bCollapse&&(d.style.height=q(a.oScroll.sY),r=""!==a.oScroll.sX&&a.nTable.offsetWidth>
+d.offsetWidth?a.oScroll.iBarWidth:0,a.nTable.offsetHeightd.clientHeight||"scroll"==h(d).css("overflow-y");b.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px";null!==a.nTFoot&&(R.style.width=q(r),l.style.width=q(r),l.style.paddingRight=c?a.oScroll.iBarWidth+"px":"0px");h(d).scroll();if(a.bSorted||a.bFiltered)d.scrollTop=0}function C(a,b,c){for(var d=
+0,i=0,f=b.length,g,e;itd",b));j=N(a,f);for(f=d=0;fc)return null;if(null===a.aoData[c].nTr){var d=l.createElement("td");d.innerHTML=v(a,c,b,"");return d}return J(a,c)[b]}function Pa(a,b){for(var c=-1,d=-1,i=0;i/g,"");e.length>c&&(c=e.length,d=i)}return d}function q(a){if(null===a)return"0px";if("number"==typeof a)return 0>a?"0px":a+"px";var b=a.charCodeAt(a.length-1);
+return 48>b||57/g,""),i=q[c].nTh,i.removeAttribute("aria-sort"),i.removeAttribute("aria-label"),q[c].bSortable?0d&&d++;f=RegExp(f+"[123]");var o;b=0;for(c=a.length;b
')[0];l.body.appendChild(b);a.oBrowser.bScrollOversize=
+100===h("#DT_BrowserTest",b)[0].offsetWidth?!0:!1;l.body.removeChild(b)}function Va(a){return function(){var b=[s(this[j.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return j.ext.oApi[a].apply(this,b)}}var U=/\[.*?\]$/,Wa=X.JSON?JSON.stringify:function(a){var b=typeof a;if("object"!==b||null===a)return"string"===b&&(a='"'+a+'"'),a+"";var c,d,e=[],f=h.isArray(a);for(c in a)d=a[c],b=typeof d,"string"===b?d='"'+d+'"':"object"===b&&null!==d&&(d=Wa(d)),e.push((f?"":'"'+c+'":')+d);return(f?
+"[":"{")+e+(f?"]":"}")};this.$=function(a,b){var c,d,e=[],f;d=s(this[j.ext.iApiIndex]);var g=d.aoData,o=d.aiDisplay,k=d.aiDisplayMaster;b||(b={});b=h.extend({},{filter:"none",order:"current",page:"all"},b);if("current"==b.page){c=d._iDisplayStart;for(d=d.fnDisplayEnd();c=d.fnRecordsDisplay()&&(d._iDisplayStart-=d._iDisplayLength,0>d._iDisplayStart&&(d._iDisplayStart=0));if(c===n||c)y(d),x(d);return g};this.fnDestroy=function(a){var b=s(this[j.ext.iApiIndex]),c=b.nTableWrapper.parentNode,d=b.nTBody,i,f,a=a===n?!1:a;b.bDestroying=!0;A(b,"aoDestroyCallback","destroy",[b]);if(!a){i=0;for(f=b.aoColumns.length;itr>td."+b.oClasses.sRowEmpty,b.nTable).parent().remove();b.nTable!=b.nTHead.parentNode&&(h(b.nTable).children("thead").remove(),b.nTable.appendChild(b.nTHead));b.nTFoot&&b.nTable!=b.nTFoot.parentNode&&(h(b.nTable).children("tfoot").remove(),b.nTable.appendChild(b.nTFoot));b.nTable.parentNode.removeChild(b.nTable);h(b.nTableWrapper).remove();b.aaSorting=[];b.aaSortingFixed=[];P(b);h(T(b)).removeClass(b.asStripeClasses.join(" "));h("th, td",b.nTHead).removeClass([b.oClasses.sSortable,b.oClasses.sSortableAsc,
+b.oClasses.sSortableDesc,b.oClasses.sSortableNone].join(" "));b.bJUI&&(h("th span."+b.oClasses.sSortIcon+", td span."+b.oClasses.sSortIcon,b.nTHead).remove(),h("th, td",b.nTHead).each(function(){var a=h("div."+b.oClasses.sSortJUIWrapper,this),c=a.contents();h(this).append(c);a.remove()}));!a&&b.nTableReinsertBefore?c.insertBefore(b.nTable,b.nTableReinsertBefore):a||c.appendChild(b.nTable);i=0;for(f=b.aoData.length;i=t(d);if(!m)for(e=a;et<"F"ip>')):h.extend(g.oClasses,j.ext.oStdClasses);h(this).addClass(g.oClasses.sTable);if(""!==g.oScroll.sX||""!==g.oScroll.sY)g.oScroll.iBarWidth=Qa();g.iInitDisplayStart===n&&(g.iInitDisplayStart=e.iDisplayStart,
+g._iDisplayStart=e.iDisplayStart);e.bStateSave&&(g.oFeatures.bStateSave=!0,Sa(g,e),z(g,"aoDrawCallback",ra,"state_save"));null!==e.iDeferLoading&&(g.bDeferLoading=!0,a=h.isArray(e.iDeferLoading),g._iRecordsDisplay=a?e.iDeferLoading[0]:e.iDeferLoading,g._iRecordsTotal=a?e.iDeferLoading[1]:e.iDeferLoading);null!==e.aaData&&(f=!0);""!==e.oLanguage.sUrl?(g.oLanguage.sUrl=e.oLanguage.sUrl,h.getJSON(g.oLanguage.sUrl,null,function(a){pa(a);h.extend(true,g.oLanguage,e.oLanguage,a);ba(g)}),i=!0):h.extend(!0,
+g.oLanguage,e.oLanguage);null===e.asStripeClasses&&(g.asStripeClasses=[g.oClasses.sStripeOdd,g.oClasses.sStripeEven]);b=g.asStripeClasses.length;g.asDestroyStripes=[];if(b){c=!1;d=h(this).children("tbody").children("tr:lt("+b+")");for(a=0;a=g.aoColumns.length&&(g.aaSorting[a][0]=0);var k=g.aoColumns[g.aaSorting[a][0]];g.aaSorting[a][2]===n&&(g.aaSorting[a][2]=0);e.aaSorting===n&&g.saved_aaSorting===n&&(g.aaSorting[a][1]=
+k.asSorting[0]);c=0;for(d=k.asSorting.length;c=parseInt(n,10)};j.fnIsDataTable=function(e){for(var h=j.settings,m=0;me)return e;for(var h=e+"",e=h.split(""),j="",h=h.length,k=0;k'+k.sPrevious+''+k.sNext+"":'';h(j).append(k);var l=h("a",j),
+k=l[0],l=l[1];e.oApi._fnBindAction(k,{action:"previous"},n);e.oApi._fnBindAction(l,{action:"next"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_previous",l.id=e.sTableId+"_next",k.setAttribute("aria-controls",e.sTableId),l.setAttribute("aria-controls",e.sTableId))},fnUpdate:function(e){if(e.aanFeatures.p)for(var h=e.oClasses,j=e.aanFeatures.p,k,l=0,n=j.length;l'+k.sFirst+''+k.sPrevious+''+k.sNext+''+k.sLast+"");var t=h("a",j),k=t[0],l=t[1],r=t[2],t=t[3];e.oApi._fnBindAction(k,{action:"first"},n);e.oApi._fnBindAction(l,{action:"previous"},n);e.oApi._fnBindAction(r,{action:"next"},n);e.oApi._fnBindAction(t,{action:"last"},n);e.aanFeatures.p||(j.id=e.sTableId+"_paginate",k.id=e.sTableId+"_first",l.id=e.sTableId+"_previous",r.id=e.sTableId+"_next",t.id=e.sTableId+"_last")},
+fnUpdate:function(e,o){if(e.aanFeatures.p){var m=j.ext.oPagination.iFullNumbersShowPages,k=Math.floor(m/2),l=Math.ceil(e.fnRecordsDisplay()/e._iDisplayLength),n=Math.ceil(e._iDisplayStart/e._iDisplayLength)+1,t="",r,B=e.oClasses,u,M=e.aanFeatures.p,L=function(h){e.oApi._fnBindAction(this,{page:h+r-1},function(h){e.oApi._fnPageChange(e,h.data.page);o(e);h.preventDefault()})};-1===e._iDisplayLength?n=k=r=1:l=l-k?(r=l-m+1,k=l):(r=n-Math.ceil(m/2)+1,k=r+m-1);for(m=r;m<=k;m++)t+=
+n!==m?''+e.fnFormatNumber(m)+"":''+e.fnFormatNumber(m)+"";m=0;for(k=M.length;mh?1:0},"string-desc":function(e,h){return eh?-1:0},"html-pre":function(e){return e.replace(/<.*?>/g,"").toLowerCase()},"html-asc":function(e,h){return eh?1:0},"html-desc":function(e,h){return e<
+h?1:e>h?-1:0},"date-pre":function(e){e=Date.parse(e);if(isNaN(e)||""===e)e=Date.parse("01/01/1970 00:00:00");return e},"date-asc":function(e,h){return e-h},"date-desc":function(e,h){return h-e},"numeric-pre":function(e){return"-"==e||""===e?0:1*e},"numeric-asc":function(e,h){return e-h},"numeric-desc":function(e,h){return h-e}});h.extend(j.ext.aTypes,[function(e){if("number"===typeof e)return"numeric";if("string"!==typeof e)return null;var h,j=!1;h=e.charAt(0);if(-1=="0123456789-".indexOf(h))return null;
+for(var k=1;k")?"html":null}]);h.fn.DataTable=j;h.fn.dataTable=j;h.fn.dataTableSettings=j.settings;h.fn.dataTableExt=j.ext};"function"===typeof define&&define.amd?define(["jquery"],L):jQuery&&!jQuery.fn.dataTable&&
+L(jQuery)})(window,document);
diff --git a/airtime_mvc/public/js/datatables/plugin/dataTables.ColReorder.js b/airtime_mvc/public/js/datatables/plugin/dataTables.ColReorder.js
index 7c7b728a2..35b96594f 100644
--- a/airtime_mvc/public/js/datatables/plugin/dataTables.ColReorder.js
+++ b/airtime_mvc/public/js/datatables/plugin/dataTables.ColReorder.js
@@ -1,8 +1,8 @@
/*
* File: ColReorder.js
- * Version: 1.0.5
+ * Version: 1.0.8
* CVS: $Id$
- * Description: Controls for column visiblity in DataTables
+ * Description: Allow columns to be reordered in a DataTable
* Author: Allan Jardine (www.sprymedia.co.uk)
* Created: Wed Sep 15 18:23:29 BST 2010
* Modified: $Date$ by $Author$
@@ -174,10 +174,10 @@ $.fn.dataTableExt.oApi.fnColReorder = function ( oSettings, iFrom, iTo )
for ( i=0, iLen=iCols ; i= iCols )
- {
- this.oApi._fnLog( oSettings, 1, "ColReorder 'from' index is out of bounds: "+iFrom );
- return;
- }
-
- if ( iTo < 0 || iTo >= iCols )
- {
- this.oApi._fnLog( oSettings, 1, "ColReorder 'to' index is out of bounds: "+iTo );
- return;
- }
-
- /*
- * Calculate the new column array index, so we have a mapping between the old and new
- */
- var aiMapping = [];
- for ( i=0, iLen=iCols ; i this.s.fixed-1 )
- {
- this._fnMouseListener( i, this.s.dt.aoColumns[i].nTh );
- }
-
- /* Mark the original column order for later reference */
- this.s.dt.aoColumns[i]._ColReorder_iOrigCol = i;
- }
-
- /* State saving */
- this.s.dt.aoStateSave.push( {
- "fn": function (oS, sVal) {
- return that._fnStateSave.call( that, sVal );
- },
- "sName": "ColReorder_State"
- } );
-
- /* An initial column order has been specified */
- var aiOrder = null;
- if ( typeof this.s.init.aiOrder != 'undefined' )
- {
- aiOrder = this.s.init.aiOrder.slice();
- }
-
- /* State loading, overrides the column order given */
- if ( this.s.dt.oLoadedState && typeof this.s.dt.oLoadedState.ColReorder != 'undefined' &&
- this.s.dt.oLoadedState.ColReorder.length == this.s.dt.aoColumns.length )
- {
- aiOrder = this.s.dt.oLoadedState.ColReorder;
- }
-
- /* If we have an order to apply - do so */
- if ( aiOrder )
- {
- /* We might be called during or after the DataTables initialisation. If before, then we need
- * to wait until the draw is done, if after, then do what we need to do right away
- */
- if ( !that.s.dt._bInitComplete )
- {
- var bDone = false;
- this.s.dt.aoDrawCallback.push( {
- "fn": function () {
- if ( !that.s.dt._bInitComplete && !bDone )
- {
- bDone = true;
- var resort = fnInvertKeyValues( aiOrder );
- that._fnOrderColumns.call( that, resort );
- }
- },
- "sName": "ColReorder_Pre"
- } );
- }
- else
- {
- var resort = fnInvertKeyValues( aiOrder );
- that._fnOrderColumns.call( that, resort );
- }
- }
- },
-
-
- /**
- * Set the column order from an array
- * @method _fnOrderColumns
- * @param array a An array of integers which dictate the column order that should be applied
- * @returns void
- * @private
- */
- "_fnOrderColumns": function ( a )
- {
- if ( a.length != this.s.dt.aoColumns.length )
- {
- this.s.dt.oInstance.oApi._fnLog( oDTSettings, 1, "ColReorder - array reorder does not "+
- "match known number of columns. Skipping." );
- return;
- }
-
- for ( var i=0, iLen=a.length ; i 0 )
- {
- this.dom.drag.removeChild( this.dom.drag.getElementsByTagName('tbody')[0] );
- }
- while ( this.dom.drag.getElementsByTagName('tfoot').length > 0 )
- {
- this.dom.drag.removeChild( this.dom.drag.getElementsByTagName('tfoot')[0] );
- }
-
- $('thead tr:eq(0)', this.dom.drag).each( function () {
- $('th:not(:eq('+that.s.mouse.targetIndex+'))', this).remove();
- } );
- $('tr', this.dom.drag).height( $('tr:eq(0)', that.s.dt.nTHead).height() );
-
- $('thead tr:gt(0)', this.dom.drag).remove();
-
- $('thead th:eq(0)', this.dom.drag).each( function (i) {
- this.style.width = $('th:eq('+that.s.mouse.targetIndex+')', that.s.dt.nTHead).width()+"px";
- } );
-
- this.dom.drag.style.position = "absolute";
- this.dom.drag.style.zIndex = 1200;
- this.dom.drag.style.top = "0px";
- this.dom.drag.style.left = "0px";
- this.dom.drag.style.width = $('th:eq('+that.s.mouse.targetIndex+')', that.s.dt.nTHead).outerWidth()+"px";
-
-
- this.dom.pointer = document.createElement( 'div' );
- this.dom.pointer.className = "DTCR_pointer";
- this.dom.pointer.style.position = "absolute";
-
- if ( this.s.dt.oScroll.sX === "" && this.s.dt.oScroll.sY === "" )
- {
- this.dom.pointer.style.top = $(this.s.dt.nTable).offset().top+"px";
- this.dom.pointer.style.height = $(this.s.dt.nTable).height()+"px";
- }
- else
- {
- this.dom.pointer.style.top = $('div.dataTables_scroll', this.s.dt.nTableWrapper).offset().top+"px";
- this.dom.pointer.style.height = $('div.dataTables_scroll', this.s.dt.nTableWrapper).height()+"px";
- }
-
- document.body.appendChild( this.dom.pointer );
- document.body.appendChild( this.dom.drag );
- }
-};
-
-
-
-
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Static parameters
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/**
- * Array of all ColReorder instances for later reference
- * @property ColReorder.aoInstances
- * @type array
- * @default []
- * @static
- */
-ColReorder.aoInstances = [];
-
-
-
-
-
-/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * Static functions
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-/**
- * Reset the column ordering for a DataTables instance
- * @method ColReorder.fnReset
- * @param object oTable DataTables instance to consider
- * @returns void
- * @static
- */
-ColReorder.fnReset = function ( oTable )
-{
- for ( var i=0, iLen=ColReorder.aoInstances.length ; i Tue, 19 Mar 2013 16:39:23 +0000
+
+airtime (2.3.0-2) unstable; urgency=low
+
+ * Don't run the airtime-install script if the user has chosen not to
+ set up Apache
+
+ -- Daniel James Tue, 19 Mar 2013 16:39:23 +0000
+
airtime (2.3.0-1) unstable; urgency=low
- * Nightly development snapshot of Airtime 2.3.x
+ * Upstream 2.3.0 release
- -- Daniel James Tue, 22 Jan 2013 11:44:57 +0000
+ -- Daniel James Tue, 12 Feb 2013 11:44:57 +0000
airtime (2.2.1-1) unstable; urgency=low
diff --git a/debian/control b/debian/control
index 4a8ad6e96..fc1fc9756 100644
--- a/debian/control
+++ b/debian/control
@@ -41,7 +41,7 @@ Depends: apache2,
pwgen,
python,
rabbitmq-server,
- silan,
+ silan (>= 0.3.1~),
sudo,
sysv-rc,
tar (>= 1.22),
@@ -49,7 +49,7 @@ Depends: apache2,
vorbis-tools,
zendframework | libzend-framework-php,
${misc:Depends}
-Recommends: icecast2
+Recommends: icecast2, php-apc
Suggests: airtime-audio-samples,
alsa-utils
Description: open radio software for scheduling and remote station management.
diff --git a/debian/postinst b/debian/postinst
index e6d5169f1..3f30de56a 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -1,10 +1,14 @@
#!/bin/bash
#postinst script for airtime
-set -x
+set -e
. /usr/share/debconf/confmodule
+if [ "$DPKG_DEBUG" = "developer" ]; then
+ set -x
+fi
+
wwwdir="/usr/share/airtime"
tmpdir="/var/lib/airtime/tmp"
configdir="/etc/airtime"
@@ -12,7 +16,7 @@ includefile="${configdir}/apache.conf"
a2tplfile="${configdir}/apache.vhost.tpl"
phpinifile="${configdir}/airtime.ini"
OLDVERSION="$2"
-NEWVERSION="2.3.0"
+NEWVERSION="2.4.0"
case "$1" in
configure|reconfigure)
@@ -20,39 +24,30 @@ case "$1" in
webserver="apache2"
php="php5"
- # clean up previous configurations
- if [ -L /etc/$webserver/conf.d/airtime.conf ]; then
- rm -f /etc/$webserver/conf.d/airtime.conf
- fi
-
- if [ -f /etc/$webserver/sites-available/airtime-vhost ]; then
- a2dissite airtime-vhost
- fi
-
# this file in 1.8.2 is a directory path in 1.9.3
if [ -f /var/www/airtime/utils/airtime-import ]; then
rm -f /var/www/airtime/utils/airtime-import
fi
- # APACHE config
- echo "Setting up apache2..."
-
- # create the document root if it doesn't exist
- if [ ! -d $wwwdir/public/ ]; then
- install -d -m755 $wwwdir/public/
- fi
-
- # set up the virtual host
+ # do we set up a virtual host?
db_get airtime/apache-setup
- if [ "$RET" == "system-wide (all vhosts)" ]; then
- if [ ! -d /etc/$webserver/conf.d/ ]; then
- install -d -m755 /etc/$webserver/conf.d/
- fi
- if [ ! -e /etc/$webserver/conf.d/airtime.conf ]; then
- ln -s ${includefile} /etc/$webserver/conf.d/airtime.conf
+ APACHESETUP=$RET
+ if [ "${APACHESETUP}" == "no thanks" ]; then
+ echo "Not setting up ${webserver} and ${php}..."
+
+ elif [ "${APACHESETUP}" == "dedicated v-host" ]; then
+ echo "Setting up ${webserver}..."
+
+ # create the document root if it doesn't exist
+ if [ ! -d $wwwdir/public/ ]; then
+ install -d -m755 $wwwdir/public/
+ fi
+
+ # temporarily disable an existing virtual host
+ if [ -f /etc/$webserver/sites-available/airtime-vhost ]; then
+ a2dissite airtime-vhost
fi
- elif [ "$RET" == "dedicated v-host" ]; then
db_get airtime/apache-servername
SN=$RET
db_get airtime/apache-serveradmin
@@ -69,30 +64,49 @@ case "$1" in
if [ $RETVAL -eq 0 ]; then
a2ensite airtime-vhost
fi
- fi
- # enable the rewrite module
- command -v a2enmod > /dev/null
- RETVAL=$?
- if [ $RETVAL -eq 0 ]; then
- a2enmod rewrite
- fi
+ # insert a specific hostname, if provided, into API configuration
+ if [ "${SN}" != "localhost" ]; then
- # remove the default site, if requested to
- db_get airtime/apache-deldefault
- if [ "$RET" == "remove default" ]; then
- if [ -f /etc/apache2/sites-available/default ]; then
- a2dissite default
+ # new installs
+ if [ -f /var/lib/airtime/tmp/airtime_mvc/build/airtime.conf -a -f /var/lib/airtime/tmp/python_apps/api_clients/api_client.cfg ]; then
+ sed -i "s/base_url = localhost/base_url = ${SN}/" /var/lib/airtime/tmp/airtime_mvc/build/airtime.conf
+ sed -i "s/host = 'localhost'/host = '${SN}'/" /var/lib/airtime/tmp/python_apps/api_clients/api_client.cfg
+ fi
+
+ # upgrades
+ if [ -f /etc/airtime/airtime.conf -a -f /etc/airtime/api_client.cfg ]; then
+ sed -i "s/base_url = localhost/base_url = ${SN}/" /etc/airtime/airtime.conf
+ sed -i "s/host = 'localhost'/host = '${SN}'/" /etc/airtime/api_client.cfg
+ fi
fi
- fi
- # PHP config
- echo "Configuring php5..."
- if [ ! -d /etc/$php/conf.d/ ]; then
- install -d -m755 /etc/$php/conf.d/
- fi
- if [ ! -e /etc/$php/conf.d/airtime.ini ]; then
- ln -s ${phpinifile} /etc/$php/conf.d/airtime.ini
+ # enable the rewrite module
+ command -v a2enmod > /dev/null
+ RETVAL=$?
+ if [ $RETVAL -eq 0 ]; then
+ a2enmod rewrite
+ fi
+
+ # remove the default site, if requested to
+ db_get airtime/apache-deldefault
+ if [ "$RET" == "remove default" ]; then
+ if [ -f /etc/apache2/sites-available/default ]; then
+ a2dissite default
+ fi
+ fi
+
+ # PHP config
+ echo "Configuring php5..."
+ if [ ! -d /etc/$php/conf.d/ ]; then
+ install -d -m755 /etc/$php/conf.d/
+ fi
+ if [ ! -e /etc/$php/conf.d/airtime.ini ]; then
+ ln -s ${phpinifile} /etc/$php/conf.d/airtime.ini
+ fi
+
+ # restart apache
+ invoke-rc.d apache2 restart
fi
# XXX ICECAST XXX
@@ -186,9 +200,6 @@ case "$1" in
echo "The rabbitmq-server package does not appear to be installed on this server."
fi
- # restart apache
- invoke-rc.d apache2 restart
-
# fix the Liquidsoap symlink if it doesn't point to standard location
if [ -h /usr/bin/airtime-liquidsoap ]; then
SYMLINK_TARGET=`readlink /usr/bin/airtime-liquidsoap`
@@ -214,8 +225,12 @@ case "$1" in
echo "Reconfiguration complete."
else
- if [ -n "$OLDVERSION" ] && [[ "${OLDVERSION:0:3}" < "1.9" ]]; then
- echo "Upgrades from Airtime versions before 1.9.0 are not supported. Please back up your files and perform a clean install."
+ if [ -n "$OLDVERSION" ] && [[ "${OLDVERSION:0:3}" < "2.1" ]]; then
+ echo "Upgrades from Airtime versions before 2.1.0 are not supported. Please back up your files and perform a clean install."
+
+ elif [ "${APACHESETUP}" == "no thanks" ]; then
+ echo "Please run the ${tmpdir}/install_minimal/airtime-install script after you have set up the web server."
+
else
mkdir -p /var/log/airtime
diff --git a/install_full/ubuntu/airtime-full-install b/install_full/ubuntu/airtime-full-install
index 08596a7e9..d574b9adb 100755
--- a/install_full/ubuntu/airtime-full-install
+++ b/install_full/ubuntu/airtime-full-install
@@ -2,6 +2,9 @@
#
# Auto install script for airtime on Ubuntu
#
+# NGINX changes are contributed by Eugene MechanisM
+# Link to the post:
+# http://forum.sourcefabric.org/discussion/13563/first-step-to-run-airtime-via-nginx-based-on-airtime-2.0-beta-files
exec > >(tee install_log.txt)
exec 2>&1
@@ -11,6 +14,8 @@ if [ "$(id -u)" != "0" ]; then
exit 1
fi
+server="$1"
+
#Current dir
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=`readlink -f $0`
@@ -48,7 +53,7 @@ fi
apt-get update
# Updated package list
-apt-get -y --force-yes install tar gzip curl apache2 php5-pgsql libapache2-mod-php5 \
+apt-get -y --force-yes install tar gzip curl php5-pgsql \
php-pear php5-gd postgresql odbc-postgresql python libsoundtouch-ocaml \
libtaglib-ocaml libao-ocaml libmad-ocaml ecasound \
libesd0 libportaudio2 libsamplerate0 rabbitmq-server patch \
@@ -56,11 +61,11 @@ php5-curl mpg123 monit python-virtualenv multitail libcamomile-ocaml-data \
libpulse0 vorbis-tools lsb-release lsof sudo mp3gain vorbisgain flac vorbis-tools \
pwgen libfaad2
+
#install packages with --force-yes option (this is useful in the case
#of Debian, where these packages are unauthorized)
apt-get -y --force-yes install libmp3lame-dev lame icecast2
-
#Debian Squeeze only has zendframework package. Newer versions of Ubuntu have zend-framework package.
#Ubuntu Lucid has both zendframework and zend-framework. Difference appears to be that zendframework is for
#1.10 and zend-framework is 1.11
@@ -70,32 +75,61 @@ else
apt-get -y --force-yes install libzend-framework-php
fi
+#get the "timeout" unix command
if [ "$code" = "lucid" ]; then
apt-get -y --force-yes install timeout
else
apt-get -y --force-yes install coreutils
fi
-
#Install Sourcefabric's custom Liquidsoap debian package
apt-get -y --force-yes install sourcefabric-keyring
apt-get -y --force-yes install liquidsoap
+apt-get -y --force-yes install silan
+if [ "$server" = "nginx" ]; then
+ apt-get -y --force-yes install nginx php5-fpm
+ # NGINX Config File
+ echo "----------------------------------------------------"
+ echo "2.1 NGINX Config File"
+ echo "----------------------------------------------------"
+ if [ ! -f /etc/nginx/sites-available/airtime ]; then
+ cp $SCRIPTPATH/../nginx/airtime-vhost /etc/nginx/sites-available/airtime
+ ln -s /etc/nginx/sites-available/airtime /etc/nginx/sites-enabled/airtime
+ service nginx reload
+ else
+ echo "NGINX config for Airtime already exists..."
+ fi
-# Apache Config File
-echo "----------------------------------------------------"
-echo "2. Apache Config File"
-echo "----------------------------------------------------"
-if [ ! -f /etc/apache2/sites-available/airtime ]; then
- cp $SCRIPTPATH/../apache/airtime-vhost /etc/apache2/sites-available/airtime
- a2dissite default
- a2ensite airtime
- a2enmod rewrite php5
- service apache2 restart
+ # php-fpm Airtime pool file
+ echo "----------------------------------------------------"
+ echo "2.2 Airtime php pool file"
+ echo "----------------------------------------------------"
+ if [ ! -f /etc/php5/fpm/pool.d/airtime.conf ]; then
+ cp $SCRIPTPATH/../php5-fpm/airtime.conf /etc/php5/fpm/pool.d/airtime.conf
+ service php5-fpm reload
+ else
+ echo "Airtime php pool file already exists..."
+ fi
else
- echo "Apache config for Airtime already exists..."
+ apt-get -y --force-yes install apache2 libapache2-mod-php5
+ # Apache Config File
+ echo "----------------------------------------------------"
+ echo "2. Apache Config File"
+ echo "----------------------------------------------------"
+ if [ ! -f /etc/apache2/sites-available/airtime ]; then
+ cp $SCRIPTPATH/../apache/airtime-vhost /etc/apache2/sites-available/airtime
+ a2dissite default
+ a2ensite airtime
+ a2enmod rewrite php5
+ service apache2 restart
+ else
+ echo "Apache config for Airtime already exists..."
+ fi
fi
+
+
# Enable Icecast
echo "----------------------------------------------------"
echo "3. Enable Icecast"
diff --git a/install_full/ubuntu/airtime-full-install-nginx b/install_full/ubuntu/airtime-full-install-nginx
index 58a7f2af4..7f55a3aa2 100755
--- a/install_full/ubuntu/airtime-full-install-nginx
+++ b/install_full/ubuntu/airtime-full-install-nginx
@@ -1,130 +1,10 @@
-#!/bin/bash -e
-#
-# Auto install script for airtime on Ubuntu
-#
-# NGINX changes are contributed by Eugene MechanisM
-# Link to the post:
-# http://forum.sourcefabric.org/discussion/13563/first-step-to-run-airtime-via-nginx-based-on-airtime-2.0-beta-files
+#!/bin/bash
+# Auto install script for airtime + nginx
-exec > >(tee install_log.txt)
-exec 2>&1
-
-if [ "$(id -u)" != "0" ]; then
- echo "Please run as root user."
- exit 1
-fi
-
-#Current dir
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=`readlink -f $0`
# Absolute path this script is in, thus /home/user/bin
SCRIPTPATH=`dirname $SCRIPT`
-#Prerequisite
-echo "----------------------------------------------------"
-echo " 1. Install Packages"
-echo "----------------------------------------------------"
-
-dist=`lsb_release -is`
-code=`lsb_release -cs`
-
-#enable squeeze backports to get lame packages
-if [ "$dist" = "Debian" -a "$code" = "squeeze" ]; then
- grep "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list
- if [ "$?" -ne "0" ]; then
- echo "deb http://backports.debian.org/debian-backports squeeze-backports main" >> /etc/apt/sources.list
- fi
-fi
-
-apt-get update
-
-# Updated package list
-apt-get -y --force-yes install tar gzip curl nginx php5-pgsql php5-fpm \
-php-pear php5-gd postgresql odbc-postgresql python libsoundtouch-ocaml \
-libtaglib-ocaml libao-ocaml libmad-ocaml ecasound \
-libesd0 libportaudio2 libsamplerate0 rabbitmq-server patch \
-php5-curl mpg123 monit python-virtualenv multitail libcamomile-ocaml-data \
-libpulse0 vorbis-tools lsb-release lsof sudo mp3gain vorbisgain flac vorbis-tools \
-pwgen libfaad2
-
-#install packages with --force-yes option (this is useful in the case
-#of Debian, where these packages are unauthorized)
-apt-get -y --force-yes install libmp3lame-dev lame icecast2
-
-#Debian Squeeze only has zendframework package. Newer versions of Ubuntu have zend-framework package.
-#Ubuntu Lucid has both zendframework and zend-framework. Difference appears to be that zendframework is for
-#1.10 and zend-framework is 1.11
-if [ "$dist" = "Debian" ]; then
- apt-get -y install --force-yes zendframework
-else
- apt-get -y install --force-yes libzend-framework-php
-fi
-
-if [ "$code" = "lucid" ]; then
- apt-get -y install --force-yes timeout
-else
- apt-get -y install --force-yes coreutils
-fi
-
-# NGINX Config File
-echo "----------------------------------------------------"
-echo "2.1 NGINX Config File"
-echo "----------------------------------------------------"
-if [ ! -f /etc/nginx/sites-available/airtime ]; then
- cp $SCRIPTPATH/../nginx/airtime-vhost /etc/nginx/sites-available/airtime
- ln -s /etc/nginx/sites-available/airtime /etc/nginx/sites-enabled/airtime
- service nginx reload
-else
- echo "NGINX config for Airtime already exists..."
-fi
-
-#Install Sourcefabric's custom Liquidsoap debian package
-apt-get -y --force-yes install sourcefabric-keyring
-apt-get -y --force-yes install liquidsoap
-
-# php-fpm Airtime pool file
-echo "----------------------------------------------------"
-echo "2.2 Airtime php pool file"
-echo "----------------------------------------------------"
-if [ ! -f /etc/php5/fpm/pool.d/airtime.conf ]; then
- cp $SCRIPTPATH/../php5-fpm/airtime.conf /etc/php5/fpm/pool.d/airtime.conf
- service php5-fpm reload
-else
- echo "Airtime php pool file already exists..."
-fi
-
-# Enable Icecast
-echo "----------------------------------------------------"
-echo "3. Enable Icecast"
-echo "----------------------------------------------------"
-cd /etc/default/
-sed -i 's/ENABLE=false/ENABLE=true/g' icecast2
-set +e
-service icecast2 start
-set -e
-echo ""
-
-# Enable Monit
-echo "----------------------------------------------------"
-echo "4. Enable Monit"
-echo "----------------------------------------------------"
-cd /etc/default/
-sed -i 's/startup=0/startup=1/g' monit
-
-set +e
-grep -q "include /etc/monit/conf.d" /etc/monit/monitrc
-RETVAL=$?
-set -e
-if [ $RETVAL -ne 0 ] ; then
- mkdir -p /etc/monit/conf.d
- echo "include /etc/monit/conf.d/*" >> /etc/monit/monitrc
-fi
-
-# Run Airtime Install
-echo "----------------------------------------------------"
-echo "5. Run Airtime Install"
-echo "----------------------------------------------------"
-cd $SCRIPTPATH/../../install_minimal
-./airtime-install
-
+$SCRIPTPATH/airtime-full-install nginx
diff --git a/install_minimal/airtime-install b/install_minimal/airtime-install
index 279fdce64..68005f54b 100755
--- a/install_minimal/airtime-install
+++ b/install_minimal/airtime-install
@@ -208,16 +208,16 @@ if [ "$mediamonitor" = "t" -o "$pypo" = "t" ]; then
fi
-/usr/lib/airtime/utils/rabbitmq-update-pid.sh > /dev/null
-
touch /usr/share/airtime/public/index.php
if [ "$python_service" -eq "0" ]; then
#only run airtime-check-system if all components were installed
echo -e "\n*** Verifying your system environment, running airtime-check-system ***"
- sleep 15
+ sleep 10
+ set +e
airtime-check-system --no-color
+ set -e
fi
echo -e "\n******************************* Install Complete *******************************"
diff --git a/install_minimal/include/airtime-constants.php b/install_minimal/include/airtime-constants.php
index cd5514f6d..10c620da9 100644
--- a/install_minimal/include/airtime-constants.php
+++ b/install_minimal/include/airtime-constants.php
@@ -1,3 +1,3 @@
/dev/null 2>&1
monit unmonitor airtime-liquidsoap >/dev/null 2>&1
monit unmonitor airtime-playout >/dev/null 2>&1
-monit unmonitor rabbitmq-server
set -e
#virtualenv_bin="/usr/lib/airtime/airtime_virtualenv/bin/"
diff --git a/install_minimal/include/airtime-upgrade.php b/install_minimal/include/airtime-upgrade.php
index 37772579a..cfa2093dc 100644
--- a/install_minimal/include/airtime-upgrade.php
+++ b/install_minimal/include/airtime-upgrade.php
@@ -108,4 +108,8 @@ if (strcmp($version, "2.3.0") < 0) {
passthru("php --php-ini $SCRIPTPATH/../airtime-php.ini $SCRIPTPATH/../upgrades/airtime-2.3.0/airtime-upgrade.php");
pause();
}
+if (strcmp($version, "2.3.1") < 0) {
+ passthru("php --php-ini $SCRIPTPATH/../airtime-php.ini $SCRIPTPATH/../upgrades/airtime-2.3.1/airtime-upgrade.php");
+ pause();
+}
echo "******************************* Upgrade Complete *******************************".PHP_EOL;
diff --git a/install_minimal/upgrades/airtime-2.3.0/data/schema.sql b/install_minimal/upgrades/airtime-2.3.0/data/schema.sql
index 689c9ed3c..b742303f0 100644
--- a/install_minimal/upgrades/airtime-2.3.0/data/schema.sql
+++ b/install_minimal/upgrades/airtime-2.3.0/data/schema.sql
@@ -1,3 +1,4 @@
+
CREATE SEQUENCE cc_listener_count_id_seq
START WITH 1
INCREMENT BY 1
@@ -55,6 +56,24 @@ ALTER TABLE cc_files
ADD COLUMN silan_check boolean DEFAULT false,
ADD COLUMN hidden boolean DEFAULT false;
+ALTER TABLE cc_schedule
+ ALTER COLUMN cue_in DROP DEFAULT,
+ ALTER COLUMN cue_in SET NOT NULL,
+ ALTER COLUMN cue_out DROP DEFAULT,
+ ALTER COLUMN cue_out SET NOT NULL;
+
+ALTER SEQUENCE cc_listener_count_id_seq
+ OWNED BY cc_listener_count.id;
+
+ALTER SEQUENCE cc_locale_id_seq
+ OWNED BY cc_locale.id;
+
+ALTER SEQUENCE cc_mount_name_id_seq
+ OWNED BY cc_mount_name.id;
+
+ALTER SEQUENCE cc_timestamp_id_seq
+ OWNED BY cc_timestamp.id;
+
ALTER TABLE cc_listener_count
ADD CONSTRAINT cc_listener_count_pkey PRIMARY KEY (id);
@@ -72,5 +91,3 @@ ALTER TABLE cc_listener_count
ALTER TABLE cc_listener_count
ADD CONSTRAINT cc_timestamp_inst_fkey FOREIGN KEY (timestamp_id) REFERENCES cc_timestamp(id) ON DELETE CASCADE;
-
-
diff --git a/install_minimal/upgrades/airtime-2.3.0/data/upgrade.sql b/install_minimal/upgrades/airtime-2.3.0/data/upgrade.sql
index c2d176b23..5ef1a1612 100644
--- a/install_minimal/upgrades/airtime-2.3.0/data/upgrade.sql
+++ b/install_minimal/upgrades/airtime-2.3.0/data/upgrade.sql
@@ -15,6 +15,17 @@ INSERT INTO cc_stream_setting ("keyname", "value", "type") VALUES ('s3_admin_pas
UPDATE cc_music_dirs SET directory = directory || '/' where id in (select id from cc_music_dirs where substr(directory, length(directory)) != '/');
UPDATE cc_files SET filepath = substring(filepath from 2) where id in (select id from cc_files where substring(filepath from 1 for 1) = '/');
+UPDATE cc_files SET cueout = length where cueout = '00:00:00';
+
+UPDATE cc_schedule SET cue_out = clip_length WHERE cue_out = '00:00:00';
+
+UPDATE cc_schedule SET fade_out = '00:00:59.9' WHERE fade_out > '00:00:59.9';
+UPDATE cc_schedule SET fade_in = '00:00:59.9' WHERE fade_in > '00:00:59.9';
+UPDATE cc_playlistcontents SET fadeout = '00:00:59.9' WHERE fadeout > '00:00:59.9';
+UPDATE cc_playlistcontents SET fadein = '00:00:59.9' WHERE fadein > '00:00:59.9';
+UPDATE cc_blockcontents SET fadeout = '00:00:59.9' WHERE fadeout > '00:00:59.9';
+UPDATE cc_blockcontents SET fadein = '00:00:59.9' WHERE fadein > '00:00:59.9';
+
INSERT INTO cc_pref("keystr", "valstr") VALUES('locale', 'en_CA');
INSERT INTO cc_pref("subjid", "keystr", "valstr") VALUES(1, 'user_locale', 'en_CA');
@@ -26,6 +37,8 @@ INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('es_ES', 'Español');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('fr_FR', 'Français');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('it_IT', 'Italiano');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('ko_KR', '한국어');
+INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('pl_PL', 'Polski');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('pt_BR', 'Português Brasileiro');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('ru_RU', 'Русский');
INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('zh_CN', '简体中文');
+INSERT INTO cc_locale (locale_code, locale_lang) VALUES ('el_GR', 'Ελληνικά');
diff --git a/install_minimal/upgrades/airtime-2.3.1/DbUpgrade.php b/install_minimal/upgrades/airtime-2.3.1/DbUpgrade.php
new file mode 100644
index 000000000..363b5776a
--- /dev/null
+++ b/install_minimal/upgrades/airtime-2.3.1/DbUpgrade.php
@@ -0,0 +1,25 @@
+&1 | grep -v \"will create implicit index\"");
+ passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/data/upgrade.sql $database 2>&1 | grep -v \"will create implicit index\"");
+ }
+}
diff --git a/install_minimal/upgrades/airtime-2.3.1/airtime-upgrade.php b/install_minimal/upgrades/airtime-2.3.1/airtime-upgrade.php
new file mode 100644
index 000000000..53470a0df
--- /dev/null
+++ b/install_minimal/upgrades/airtime-2.3.1/airtime-upgrade.php
@@ -0,0 +1,15 @@
+fetchColumn();
+
+ date_default_timezone_set($timezone);
+ }
+
+ public static function connectToDatabase($p_exitOnError = true)
+ {
+ try {
+ $con = Propel::getConnection();
+ } catch (Exception $e) {
+ echo $e->getMessage().PHP_EOL;
+ echo "Database connection problem.".PHP_EOL;
+ echo "Check if database exists with corresponding permissions.".PHP_EOL;
+ if ($p_exitOnError) {
+ exit(1);
+ }
+ return false;
+ }
+ return true;
+ }
+
+
+ public static function DbTableExists($p_name)
+ {
+ $con = Propel::getConnection();
+ try {
+ $sql = "SELECT * FROM ".$p_name." LIMIT 1";
+ $con->query($sql);
+ } catch (PDOException $e){
+ return false;
+ }
+ return true;
+ }
+
+ private static function GetAirtimeSrcDir()
+ {
+ return __DIR__."/../../../../airtime_mvc";
+ }
+
+ public static function MigrateTablesToVersion($dir, $version)
+ {
+ echo "Upgrading database, may take several minutes, please wait".PHP_EOL;
+
+ $appDir = self::GetAirtimeSrcDir();
+ $command = "php --php-ini $dir/../../airtime-php.ini ".
+ "$appDir/library/doctrine/migrations/doctrine-migrations.phar ".
+ "--configuration=$dir/common/migrations.xml ".
+ "--db-configuration=$appDir/library/doctrine/migrations/migrations-db.php ".
+ "--no-interaction migrations:migrate $version";
+ system($command);
+ }
+
+ public static function BypassMigrations($dir, $version)
+ {
+ $appDir = self::GetAirtimeSrcDir();
+ $command = "php --php-ini $dir/../../airtime-php.ini ".
+ "$appDir/library/doctrine/migrations/doctrine-migrations.phar ".
+ "--configuration=$dir/common/migrations.xml ".
+ "--db-configuration=$appDir/library/doctrine/migrations/migrations-db.php ".
+ "--no-interaction --add migrations:version $version";
+ system($command);
+ }
+
+ public static function upgradeConfigFiles(){
+
+ $configFiles = array(UpgradeCommon::CONF_FILE_AIRTIME,
+ UpgradeCommon::CONF_FILE_PYPO,
+ //this is not necessary because liquidsoap configs
+ //are automatically generated
+ //UpgradeCommon::CONF_FILE_LIQUIDSOAP,
+ UpgradeCommon::CONF_FILE_MEDIAMONITOR,
+ UpgradeCommon::CONF_FILE_API_CLIENT);
+
+ // Backup the config files
+ $suffix = date("Ymdhis")."-".UpgradeCommon::VERSION_NUMBER;
+ foreach ($configFiles as $conf) {
+ // do not back up monit cfg -- ok?? not being done anyway
+ if (file_exists($conf)) {
+ echo "Backing up $conf to $conf$suffix.bak".PHP_EOL;
+ //copy($conf, $conf.$suffix.".bak");
+ exec("cp -p $conf $conf$suffix.bak"); //use cli version to preserve file attributes
+ }
+ }
+
+ self::CreateIniFiles(UpgradeCommon::CONF_BACKUP_SUFFIX);
+ self::MergeConfigFiles($configFiles, $suffix);
+ }
+
+ /**
+ * This function creates the /etc/airtime configuration folder
+ * and copies the default config files to it.
+ */
+ public static function CreateIniFiles($suffix)
+ {
+ if (!file_exists("/etc/airtime/")){
+ if (!mkdir("/etc/airtime/", 0755, true)){
+ echo "Could not create /etc/airtime/ directory. Exiting.";
+ exit(1);
+ }
+ }
+
+ $config_copy = array(
+ "../etc/airtime.conf" => self::CONF_FILE_AIRTIME,
+ "../etc/pypo.cfg" => self::CONF_FILE_PYPO,
+ "../etc/media-monitor.cfg" => self::CONF_FILE_MEDIAMONITOR,
+ "../etc/api_client.cfg" => self::CONF_FILE_API_CLIENT
+ );
+
+ echo "Copying configs:\n";
+ foreach ($config_copy as $path_part => $destination) {
+ $full_path = OsPath::normpath(OsPath::join(__DIR__,
+ "$path_part.$suffix"));
+ echo "'$full_path' --> '$destination'\n";
+ if(!copy($full_path, $destination)) {
+ echo "Failed on the copying operation above\n";
+ exit(1);
+ }
+ }
+ }
+
+ private static function MergeConfigFiles(array $configFiles, $suffix) {
+ foreach ($configFiles as $conf) {
+ if (file_exists("$conf$suffix.bak")) {
+
+ if($conf === self::CONF_FILE_AIRTIME) {
+ // Parse with sections
+ $newSettings = parse_ini_file($conf, true);
+ $oldSettings = parse_ini_file("$conf$suffix.bak", true);
+ }
+ else {
+ $newSettings = self::ReadPythonConfig($conf);
+ $oldSettings = self::ReadPythonConfig("$conf$suffix.bak");
+ }
+
+ $settings = array_keys($newSettings);
+
+ foreach($settings as $section) {
+ if(isset($oldSettings[$section])) {
+ if(is_array($oldSettings[$section])) {
+ $sectionKeys = array_keys($newSettings[$section]);
+ foreach($sectionKeys as $sectionKey) {
+
+ if(isset($oldSettings[$section][$sectionKey])) {
+ self::UpdateIniValue($conf, $sectionKey,
+ $oldSettings[$section][$sectionKey]);
+ }
+ }
+ } else {
+ self::UpdateIniValue($conf, $section,
+ $oldSettings[$section]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static function ReadPythonConfig($p_filename)
+ {
+ $values = array();
+
+ $fh = fopen($p_filename, 'r');
+
+ while(!feof($fh)){
+ $line = fgets($fh);
+ if(substr(trim($line), 0, 1) == '#' || trim($line) == ""){
+ continue;
+ }else{
+ $info = explode('=', $line, 2);
+ $values[trim($info[0])] = trim($info[1]);
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * This function updates an INI style config file.
+ *
+ * A property and the value the property should be changed to are
+ * supplied. If the property is not found, then no changes are made.
+ *
+ * @param string $p_filename
+ * The path the to the file.
+ * @param string $p_property
+ * The property to look for in order to change its value.
+ * @param string $p_value
+ * The value the property should be changed to.
+ *
+ */
+ private static function UpdateIniValue($p_filename, $p_property, $p_value)
+ {
+ $lines = file($p_filename);
+ $n = count($lines);
+ foreach ($lines as &$line) {
+ if ($line[0] != "#"){
+ $key_value = explode("=", $line);
+ $key = trim($key_value[0]);
+
+ if ($key == $p_property){
+ $line = "$p_property = $p_value".PHP_EOL;
+ }
+ }
+ }
+
+ $fp=fopen($p_filename, 'w');
+ for($i=0; $i<$n; $i++){
+ fwrite($fp, $lines[$i]);
+ }
+ fclose($fp);
+ }
+
+ public static function queryDb($p_sql){
+ $con = Propel::getConnection();
+
+ try {
+ $result = $con->query($p_sql);
+ } catch (Exception $e) {
+ echo "Error executing $p_sql. Exiting.";
+ exit(1);
+ }
+
+ return $result;
+ }
+}
+
+class OsPath {
+ // this function is from http://stackoverflow.com/questions/2670299/is-there-a-php-equivalent-function-to-the-python-os-path-normpath
+ public static function normpath($path)
+ {
+ if (empty($path))
+ return '.';
+
+ if (strpos($path, '/') === 0)
+ $initial_slashes = true;
+ else
+ $initial_slashes = false;
+ if (
+ ($initial_slashes) &&
+ (strpos($path, '//') === 0) &&
+ (strpos($path, '///') === false)
+ )
+ $initial_slashes = 2;
+ $initial_slashes = (int) $initial_slashes;
+
+ $comps = explode('/', $path);
+ $new_comps = array();
+ foreach ($comps as $comp)
+ {
+ if (in_array($comp, array('', '.')))
+ continue;
+ if (
+ ($comp != '..') ||
+ (!$initial_slashes && !$new_comps) ||
+ ($new_comps && (end($new_comps) == '..'))
+ )
+ array_push($new_comps, $comp);
+ elseif ($new_comps)
+ array_pop($new_comps);
+ }
+ $comps = $new_comps;
+ $path = implode('/', $comps);
+ if ($initial_slashes)
+ $path = str_repeat('/', $initial_slashes) . $path;
+ if ($path)
+ return $path;
+ else
+ return '.';
+ }
+
+ /* Similar to the os.path.join python method
+ * http://stackoverflow.com/a/1782990/276949 */
+ public static function join() {
+ $args = func_get_args();
+ $paths = array();
+
+ foreach($args as $arg) {
+ $paths = array_merge($paths, (array)$arg);
+ }
+
+ foreach($paths as &$path) {
+ $path = trim($path, DIRECTORY_SEPARATOR);
+ }
+
+ if (substr($args[0], 0, 1) == DIRECTORY_SEPARATOR) {
+ $paths[0] = DIRECTORY_SEPARATOR . $paths[0];
+ }
+
+ return join(DIRECTORY_SEPARATOR, $paths);
+ }
+}
diff --git a/install_minimal/upgrades/airtime-2.3.1/data/upgrade.sql b/install_minimal/upgrades/airtime-2.3.1/data/upgrade.sql
new file mode 100644
index 000000000..6b3bb0a42
--- /dev/null
+++ b/install_minimal/upgrades/airtime-2.3.1/data/upgrade.sql
@@ -0,0 +1,2 @@
+DELETE FROM cc_pref WHERE keystr = 'system_version';
+INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.3.1');
diff --git a/python_apps/api_clients/api_client.cfg b/python_apps/api_clients/api_client.cfg
index d9a67b608..0f1050a61 100644
--- a/python_apps/api_clients/api_client.cfg
+++ b/python_apps/api_clients/api_client.cfg
@@ -115,7 +115,7 @@ get_bootstrap_info = 'get-bootstrap-info/format/json/api_key/%%api_key%%'
get_files_without_replay_gain = 'get-files-without-replay-gain/api_key/%%api_key%%/dir_id/%%dir_id%%'
-update_replay_gain_value = 'update-replay-gain-value/api_key/%%api_key%%'
+update_replay_gain_value = 'update-replay-gain-value/format/json/api_key/%%api_key%%'
notify_webstream_data = 'notify-webstream-data/api_key/%%api_key%%/media_id/%%media_id%%/format/json'
diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py
index 048da729c..be80eb0ca 100644
--- a/python_apps/api_clients/api_client.py
+++ b/python_apps/api_clients/api_client.py
@@ -15,7 +15,7 @@ import json
import base64
from configobj import ConfigObj
-AIRTIME_VERSION = "2.3.0"
+AIRTIME_VERSION = "2.3.1"
# TODO : Place these functions in some common module. Right now, media
@@ -73,21 +73,30 @@ class ApcUrl(object):
else: return self.base_url
class ApiRequest(object):
- def __init__(self, name, url):
+ def __init__(self, name, url, logger=None):
self.name = name
self.url = url
self.__req = None
+ if logger is None: self.logger = logging
+ else: self.logger = logger
def __call__(self,_post_data=None, **kwargs):
- # TODO : get rid of god damn urllib and replace everything with
- # grequests or requests at least
final_url = self.url.params(**kwargs).url()
if _post_data is not None: _post_data = urllib.urlencode(_post_data)
- req = urllib2.Request(final_url, _post_data)
- response = urllib2.urlopen(req).read()
+ try:
+ req = urllib2.Request(final_url, _post_data)
+ response = urllib2.urlopen(req).read()
+ except Exception, e:
+ import traceback
+ self.logger.error('Exception: %s', e)
+ self.logger.error("traceback: %s", traceback.format_exc())
+ raise
# Ghetto hack for now because we don't the content type we are getting
# (Pointless to look at mime since it's not being set correctly always)
- try: return json.loads(response)
- except ValueError: return response
+ try:
+ return json.loads(response)
+ except Exception:
+ self.logger.error(response)
+ raise
def req(self, *args, **kwargs):
self.__req = lambda : self(*args, **kwargs)
@@ -107,7 +116,9 @@ class RequestProvider(object):
def __init__(self, cfg):
self.config = cfg
self.requests = {}
- self.url = ApcUrl("http://%s:%s/%s/%s/%s" \
+ if self.config["base_dir"].startswith("/"):
+ self.config["base_dir"] = self.config["base_dir"][1:]
+ self.url = ApcUrl("http://%s:%s/%s%s/%s" \
% (self.config["host"], str(self.config["base_port"]),
self.config["base_dir"], self.config["api_base"],
'%%action%%'))
@@ -141,7 +152,6 @@ class AirtimeApiClient(object):
sys.exit(1)
def __get_airtime_version(self):
- # TODO : maybe fix this function to drop an exception?
try: return self.services.version_url()[u'version']
except Exception: return -1
@@ -149,18 +159,18 @@ class AirtimeApiClient(object):
logger = self.logger
version = self.__get_airtime_version()
# logger.info('Airtime version found: ' + str(version))
- if (version == -1):
+ if version == -1:
if (verbose):
logger.info('Unable to get Airtime version number.\n')
return False
- elif (version[0:3] != AIRTIME_VERSION[0:3]):
- if (verbose):
+ elif version[0:3] != AIRTIME_VERSION[0:3]:
+ if verbose:
logger.info('Airtime version found: ' + str(version))
logger.info('pypo is at version ' + AIRTIME_VERSION +
' and is not compatible with this version of Airtime.\n')
return False
else:
- if (verbose):
+ if verbose:
logger.info('Airtime version: ' + str(version))
logger.info('pypo is at version ' + AIRTIME_VERSION + ' and is compatible with this version of Airtime.')
return True
@@ -168,29 +178,32 @@ class AirtimeApiClient(object):
def get_schedule(self):
# TODO : properly refactor this routine
- # For now thre return type is a little fucked for compatibility reasons
+ # For now the return type is a little fucked for compatibility reasons
try: return (True, self.services.export_url())
- except: (False, "")
+ except: return (False, None)
def notify_liquidsoap_started(self):
- return self.services.notify_liquidsoap_started()
+ try:
+ self.services.notify_liquidsoap_started()
+ except Exception, e:
+ self.logger.error(str(e))
def notify_media_item_start_playing(self, media_id):
""" This is a callback from liquidsoap, we use this to notify
about the currently playing *song*. We get passed a JSON string
which we handed to liquidsoap in get_liquidsoap_data(). """
- return self.services.update_start_playing_url(media_id=media_id)
-
- # TODO : get this routine out of here it doesn't belong at all here
- def get_liquidsoap_data(self, pkey, schedule):
- playlist = schedule[pkey]
- data = dict()
- try: data["schedule_id"] = playlist['id']
- except Exception: data["schedule_id"] = 0
- return data
+ try:
+ return self.services.update_start_playing_url(media_id=media_id)
+ except Exception, e:
+ self.logger.error(str(e))
+ return None
def get_shows_to_record(self):
- return self.services.show_schedule_url()
+ try:
+ return self.services.show_schedule_url()
+ except Exception, e:
+ self.logger.error(str(e))
+ return None
def upload_recorded_show(self, data, headers):
logger = self.logger
@@ -226,19 +239,29 @@ class AirtimeApiClient(object):
return response
def check_live_stream_auth(self, username, password, dj_type):
- return self.services.check_live_stream_auth(
- username=username, password=password, djtype=dj_type)
+ try:
+ return self.services.check_live_stream_auth(
+ username=username, password=password, djtype=dj_type)
+ except Exception, e:
+ self.logger.error(str(e))
+ return {}
def construct_url(self,config_action_key):
"""Constructs the base url for every request"""
# TODO : Make other methods in this class use this this method.
- url = "http://%s:%s/%s/%s/%s" % \
+ if self.config["base_dir"].startswith("/"):
+ self.config["base_dir"] = self.config["base_dir"][1:]
+ url = "http://%s:%s/%s%s/%s" % \
(self.config["host"], str(self.config["base_port"]),
self.config["base_dir"], self.config["api_base"],
self.config[config_action_key])
url = url.replace("%%api_key%%", self.config["api_key"])
return url
+ """
+ Caller of this method needs to catch any exceptions such as
+ ValueError thrown by json.loads or URLError by urllib2.urlopen
+ """
def setup_media_monitor(self):
return self.services.media_setup_url()
@@ -299,25 +322,40 @@ class AirtimeApiClient(object):
self.logger.error("Could not find index 'files' in dictionary: %s",
str(response))
return []
-
+ """
+ Caller of this method needs to catch any exceptions such as
+ ValueError thrown by json.loads or URLError by urllib2.urlopen
+ """
def list_all_watched_dirs(self):
return self.services.list_all_watched_dirs()
+ """
+ Caller of this method needs to catch any exceptions such as
+ ValueError thrown by json.loads or URLError by urllib2.urlopen
+ """
def add_watched_dir(self, path):
return self.services.add_watched_dir(path=base64.b64encode(path))
+ """
+ Caller of this method needs to catch any exceptions such as
+ ValueError thrown by json.loads or URLError by urllib2.urlopen
+ """
def remove_watched_dir(self, path):
return self.services.remove_watched_dir(path=base64.b64encode(path))
+ """
+ Caller of this method needs to catch any exceptions such as
+ ValueError thrown by json.loads or URLError by urllib2.urlopen
+ """
def set_storage_dir(self, path):
return self.services.set_storage_dir(path=base64.b64encode(path))
+ """
+ Caller of this method needs to catch any exceptions such as
+ ValueError thrown by json.loads or URLError by urllib2.urlopen
+ """
def get_stream_setting(self):
- logger = self.logger
- try: return self.services.get_stream_setting()
- except Exception, e:
- logger.error("Exception: %s", e)
- return None
+ return self.services.get_stream_setting()
def register_component(self, component):
""" Purpose of this method is to contact the server with a "Hey its
@@ -334,6 +372,7 @@ class AirtimeApiClient(object):
self.services.update_liquidsoap_status.req(msg=encoded_msg, stream_id=stream_id,
boot_time=time).retry(5)
except Exception, e:
+ #TODO
logger.error("Exception: %s", e)
def notify_source_status(self, sourcename, status):
@@ -342,10 +381,11 @@ class AirtimeApiClient(object):
return self.services.update_source_status.req(sourcename=sourcename,
status=status).retry(5)
except Exception, e:
+ #TODO
logger.error("Exception: %s", e)
def get_bootstrap_info(self):
- """ Retrive infomations needed on bootstrap time """
+ """ Retrieve infomations needed on bootstrap time """
return self.services.get_bootstrap_info()
def get_files_without_replay_gain_value(self, dir_id):
@@ -355,15 +395,23 @@ class AirtimeApiClient(object):
to this file is the return value.
"""
#http://localhost/api/get-files-without-replay-gain/dir_id/1
- return self.services.get_files_without_replay_gain(dir_id=dir_id)
-
+ try:
+ return self.services.get_files_without_replay_gain(dir_id=dir_id)
+ except Exception, e:
+ self.logger.error(str(e))
+ return []
+
def get_files_without_silan_value(self):
"""
Download a list of files that need to have their cue in/out value
calculated. This list of files is downloaded into a file and the path
to this file is the return value.
"""
- return self.services.get_files_without_silan_value()
+ try:
+ return self.services.get_files_without_silan_value()
+ except Exception, e:
+ self.logger.error(str(e))
+ return []
def update_replay_gain_values(self, pairs):
"""
@@ -372,13 +420,14 @@ class AirtimeApiClient(object):
"""
self.logger.debug(self.services.update_replay_gain_value(
_post_data={'data': json.dumps(pairs)}))
-
+
+
def update_cue_values_by_silan(self, pairs):
"""
'pairs' is a list of pairs in (x, y), where x is the file's database
row id and y is the file's cue values in dB
"""
- print self.services.update_cue_values_by_silan(_post_data={'data': json.dumps(pairs)})
+ return self.services.update_cue_values_by_silan(_post_data={'data': json.dumps(pairs)})
def notify_webstream_data(self, data, media_id):
@@ -400,5 +449,9 @@ class AirtimeApiClient(object):
return response
def update_stream_setting_table(self, data):
- response = self.services.update_stream_setting_table(_post_data={'data': json.dumps(data)})
- return response
+ try:
+ response = self.services.update_stream_setting_table(_post_data={'data': json.dumps(data)})
+ return response
+ except Exception, e:
+ #TODO
+ self.logger.error(str(e))
diff --git a/python_apps/media-monitor/airtimefilemonitor/airtimemediamonitorbootstrap.py b/python_apps/media-monitor/airtimefilemonitor/airtimemediamonitorbootstrap.py
index 7a57a74e6..02b6cf2cf 100644
--- a/python_apps/media-monitor/airtimefilemonitor/airtimemediamonitorbootstrap.py
+++ b/python_apps/media-monitor/airtimefilemonitor/airtimemediamonitorbootstrap.py
@@ -58,18 +58,18 @@ class AirtimeMediaMonitorBootstrap():
"""
returns the path and its corresponding database row idfor all watched directories. Also
returns the Stor directory, which can be identified by its row id (always has value of "1")
-
+
Return type is a dictionary similar to:
{"1":"/srv/airtime/stor/"}
"""
def get_list_of_watched_dirs(self):
json = self.api_client.list_all_watched_dirs()
-
+
try:
return json["dirs"]
except KeyError as e:
self.logger.error("Could not find index 'dirs' in dictionary: %s", str(json))
- self.logger.error(e)
+ self.logger.error(str(e))
return {}
"""
@@ -94,7 +94,7 @@ class AirtimeMediaMonitorBootstrap():
db_known_files_set = set()
files = self.list_db_files(dir_id)
-
+
for f in files:
db_known_files_set.add(f)
diff --git a/python_apps/media-monitor/airtimefilemonitor/replaygain.py b/python_apps/media-monitor/airtimefilemonitor/replaygain.py
index cc0f148f9..f5c29a538 100644
--- a/python_apps/media-monitor/airtimefilemonitor/replaygain.py
+++ b/python_apps/media-monitor/airtimefilemonitor/replaygain.py
@@ -57,14 +57,18 @@ def get_file_type(file_path):
file_type = 'vorbis'
elif re.search(r'flac$', file_path, re.IGNORECASE):
file_type = 'flac'
+ elif re.search(r'(mp4|m4a)$', file_path, re.IGNORECASE):
+ file_type = 'mp4'
else:
mime_type = get_mime_type(file_path) == "audio/mpeg"
if 'mpeg' in mime_type:
file_type = 'mp3'
- elif 'ogg' in mime_type:
+ elif 'ogg' in mime_type or "oga" in mime_type:
file_type = 'vorbis'
elif 'flac' in mime_type:
file_type = 'flac'
+ elif 'mp4' in mime_type or "m4a" in mime_type:
+ file_type = 'mp4'
return file_type
@@ -109,6 +113,12 @@ def calculate_replay_gain(file_path):
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else:
logger.warn("metaflac not found")
+ elif file_type == 'mp4':
+ if run_process("which aacgain > /dev/null") == 0:
+ out = get_process_output('aacgain -q "%s" 2> /dev/null' % temp_file_path)
+ search = re.search(r'Recommended "Track" dB change: (.*)', out)
+ else:
+ logger.warn("aacgain not found")
else:
pass
diff --git a/python_apps/media-monitor/install/media-monitor-uninitialize.py b/python_apps/media-monitor/install/media-monitor-uninitialize.py
index 84c8fb139..c73801b46 100644
--- a/python_apps/media-monitor/install/media-monitor-uninitialize.py
+++ b/python_apps/media-monitor/install/media-monitor-uninitialize.py
@@ -13,5 +13,7 @@ try:
print "OK"
else:
print "Wasn't running"
+
+ subprocess.call("update-rc.d -f airtime-media-monitor remove".split(" "))
except Exception, e:
print e
diff --git a/python_apps/media-monitor2/media/metadata/process.py b/python_apps/media-monitor2/media/metadata/process.py
index ccaa1f41c..0cbc44a54 100644
--- a/python_apps/media-monitor2/media/metadata/process.py
+++ b/python_apps/media-monitor2/media/metadata/process.py
@@ -170,17 +170,17 @@ def normalize_mutagen(path):
md['mime'] = m.mime[0] if len(m.mime) > 0 else u''
md['path'] = normpath(path)
- # silence detect(set default queue in and out)
- try:
- command = ['silan', '-f', 'JSON', md['path']]
- proc = subprocess.Popen(command, stdout=subprocess.PIPE)
- out = proc.stdout.read()
- info = json.loads(out)
- md['cuein'] = info['sound'][0][0]
- md['cueout'] = info['sound'][-1][1]
- except Exception:
- logger = logging.getLogger()
- logger.info('silan is missing')
+ # silence detect(set default cue in and out)
+ #try:
+ #command = ['silan', '-b', '-f', 'JSON', md['path']]
+ #proc = subprocess.Popen(command, stdout=subprocess.PIPE)
+ #out = proc.communicate()[0].strip('\r\n')
+
+ #info = json.loads(out)
+ #md['cuein'] = info['sound'][0][0]
+ #md['cueout'] = info['sound'][0][1]
+ #except Exception:
+ #self.logger.debug('silan is missing')
if 'title' not in md: md['title'] = u''
return md
diff --git a/python_apps/media-monitor2/media/monitor/airtime.py b/python_apps/media-monitor2/media/monitor/airtime.py
index 19659ed21..d1124449a 100644
--- a/python_apps/media-monitor2/media/monitor/airtime.py
+++ b/python_apps/media-monitor2/media/monitor/airtime.py
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
from kombu.messaging import Exchange, Queue, Consumer
from kombu.connection import BrokerConnection
+from kombu.simple import SimpleQueue
from os.path import normpath
import json
import os
import copy
+import time
from media.monitor.exceptions import BadSongFile, InvalidMetadataElement
from media.monitor.metadata import Metadata
@@ -24,35 +26,43 @@ class AirtimeNotifier(Loggable):
"""
def __init__(self, cfg, message_receiver):
self.cfg = cfg
+ self.handler = message_receiver
+ while not self.init_rabbit_mq():
+ self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
+ time.sleep(5)
+
+ def init_rabbit_mq(self):
try:
- self.handler = message_receiver
self.logger.info("Initializing RabbitMQ message consumer...")
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(cfg["rabbitmq_host"],
- cfg["rabbitmq_user"], cfg["rabbitmq_password"],
- cfg["rabbitmq_vhost"])
+ self.connection = BrokerConnection(self.cfg["rabbitmq_host"],
+ self.cfg["rabbitmq_user"], self.cfg["rabbitmq_password"],
+ self.cfg["rabbitmq_vhost"])
channel = self.connection.channel()
- consumer = Consumer(channel, schedule_queue)
- consumer.register_callback(self.handle_message)
- consumer.consume()
+
+ self.simple_queue = SimpleQueue(channel, schedule_queue)
+
self.logger.info("Initialized RabbitMQ consumer.")
except Exception as e:
self.logger.info("Failed to initialize RabbitMQ consumer")
self.logger.error(e)
+ return False
- def handle_message(self, body, message):
+ return True
+
+
+ def handle_message(self, message):
"""
Messages received from RabbitMQ are handled here. These messages
instruct media-monitor of events such as a new directory being watched,
file metadata has been changed, or any other changes to the config of
media-monitor via the web UI.
"""
- message.ack()
- self.logger.info("Received md from RabbitMQ: %s" % str(body))
- m = json.loads(message.body)
+ self.logger.info("Received md from RabbitMQ: %s" % str(message))
+ m = json.loads(message)
# TODO : normalize any other keys that could be used to pass
# directories
if 'directory' in m: m['directory'] = normpath(m['directory'])
diff --git a/python_apps/media-monitor2/media/monitor/eventdrainer.py b/python_apps/media-monitor2/media/monitor/eventdrainer.py
index 1d3bc96f6..b551fae8e 100644
--- a/python_apps/media-monitor2/media/monitor/eventdrainer.py
+++ b/python_apps/media-monitor2/media/monitor/eventdrainer.py
@@ -1,19 +1,26 @@
import socket
+import time
from media.monitor.log import Loggable
from media.monitor.toucher import RepeatTimer
+from amqplib.client_0_8.exceptions import AMQPConnectionException
class EventDrainer(Loggable):
"""
Flushes events from RabbitMQ that are sent from airtime every
certain amount of time
"""
- def __init__(self, connection, interval=1):
+ def __init__(self, airtime_notifier, interval=1):
def cb():
- # TODO : make 0.3 parameter configurable
- try : connection.drain_events(timeout=0.3)
- except socket.timeout : pass
- except Exception as e :
- self.fatal_exception("Error flushing events", e)
+ try:
+ message = airtime_notifier.simple_queue.get(block=True)
+ airtime_notifier.handle_message(message.payload)
+ message.ack()
+ except (IOError, AttributeError, AMQPConnectionException), e:
+ self.logger.error('Exception: %s', e)
+ while not airtime_notifier.init_rabbit_mq():
+ self.logger.error("Error connecting to RabbitMQ Server. \
+ Trying again in few seconds")
+ time.sleep(5)
t = RepeatTimer(interval, cb)
t.daemon = True
diff --git a/python_apps/media-monitor2/media/monitor/pure.py b/python_apps/media-monitor2/media/monitor/pure.py
index d3b6ded30..86b9a1f69 100644
--- a/python_apps/media-monitor2/media/monitor/pure.py
+++ b/python_apps/media-monitor2/media/monitor/pure.py
@@ -166,8 +166,10 @@ def walk_supported(directory, clean_empties=False):
def file_locked(path):
- f = Popen(["lsof", path], stdout=PIPE).stdout
- return bool(f.readlines())
+ #Capture stderr to avoid polluting py-interpreter.log
+ proc = Popen(["lsof", path], stdout=PIPE, stderr=PIPE)
+ out = proc.communicate()[0].strip('\r\n')
+ return bool(out)
def magic_move(old, new, after_dir_make=lambda : None):
""" Moves path old to new and constructs the necessary to
@@ -413,6 +415,7 @@ def file_playable(pathname):
""" Returns True if 'pathname' is playable by liquidsoap. False
otherwise. """
+ #currently disabled because this confuses inotify....
return True
#remove all write permissions. This is due to stupid taglib library bug
#where all files are opened in write mode. The only way around this is to
diff --git a/python_apps/media-monitor2/media/monitor/syncdb.py b/python_apps/media-monitor2/media/monitor/syncdb.py
index 786fb1a39..dd837b2a1 100644
--- a/python_apps/media-monitor2/media/monitor/syncdb.py
+++ b/python_apps/media-monitor2/media/monitor/syncdb.py
@@ -19,31 +19,35 @@ class AirtimeDB(Loggable):
saas = user().root_path
- # dirs_setup is a dict with keys:
- # u'watched_dirs' and u'stor' which point to lists of corresponding
- # dirs
- dirs_setup = self.apc.setup_media_monitor()
- dirs_setup[u'stor'] = normpath( join(saas, dirs_setup[u'stor'] ) )
- dirs_setup[u'watched_dirs'] = map(lambda p: normpath(join(saas,p)),
- dirs_setup[u'watched_dirs'])
- dirs_with_id = dict([ (k,normpath(v)) for k,v in
- self.apc.list_all_watched_dirs()['dirs'].iteritems() ])
+ try:
+ # dirs_setup is a dict with keys:
+ # u'watched_dirs' and u'stor' which point to lists of corresponding
+ # dirs
+ dirs_setup = self.apc.setup_media_monitor()
+ dirs_setup[u'stor'] = normpath( join(saas, dirs_setup[u'stor'] ) )
+ dirs_setup[u'watched_dirs'] = map(lambda p: normpath(join(saas,p)),
+ dirs_setup[u'watched_dirs'])
+ dirs_with_id = dict([ (k,normpath(v)) for k,v in
+ self.apc.list_all_watched_dirs()['dirs'].iteritems() ])
- self.id_to_dir = dirs_with_id
- self.dir_to_id = dict([ (v,k) for k,v in dirs_with_id.iteritems() ])
+ self.id_to_dir = dirs_with_id
+ self.dir_to_id = dict([ (v,k) for k,v in dirs_with_id.iteritems() ])
- self.base_storage = dirs_setup[u'stor']
- self.storage_paths = mmp.expand_storage( self.base_storage )
- self.base_id = self.dir_to_id[self.base_storage]
+ self.base_storage = dirs_setup[u'stor']
+ self.storage_paths = mmp.expand_storage( self.base_storage )
+ self.base_id = self.dir_to_id[self.base_storage]
- # hack to get around annoying schema of airtime db
- self.dir_to_id[ self.recorded_path() ] = self.base_id
- self.dir_to_id[ self.import_path() ] = self.base_id
+ # hack to get around annoying schema of airtime db
+ self.dir_to_id[ self.recorded_path() ] = self.base_id
+ self.dir_to_id[ self.import_path() ] = self.base_id
+
+ # We don't know from the x_to_y dict which directory is watched or
+ # store...
+ self.watched_directories = set([ os.path.normpath(p) for p in
+ dirs_setup[u'watched_dirs'] ])
+ except Exception, e:
+ self.logger.info(str(e))
- # We don't know from the x_to_y dict which directory is watched or
- # store...
- self.watched_directories = set([ os.path.normpath(p) for p in
- dirs_setup[u'watched_dirs'] ])
def to_id(self, directory):
""" directory path -> id """
diff --git a/python_apps/media-monitor2/media/saas/launcher.py b/python_apps/media-monitor2/media/saas/launcher.py
index 6aacb8e40..c561464e3 100644
--- a/python_apps/media-monitor2/media/saas/launcher.py
+++ b/python_apps/media-monitor2/media/saas/launcher.py
@@ -75,7 +75,7 @@ class MM2(InstanceThread, Loggable):
airtime_receiver.new_watch({ 'directory':watch_dir }, restart=True)
else: self.logger.info("Failed to add watch on %s" % str(watch_dir))
- EventDrainer(airtime_notifier.connection,
+ EventDrainer(airtime_notifier,
interval=float(config['rmq_event_wait']))
# Launch the toucher that updates the last time when the script was
@@ -85,7 +85,15 @@ class MM2(InstanceThread, Loggable):
ToucherThread(path=user().touch_file_path(),
interval=int(config['touch_interval']))
- apiclient.register_component('media-monitor')
+ success = False
+ while not success:
+ try:
+ apiclient.register_component('media-monitor')
+ success = True
+ except Exception, e:
+ self.logger.error(str(e))
+ import time
+ time.sleep(10)
manager.loop()
diff --git a/python_apps/pypo/airtime-liquidsoap b/python_apps/pypo/airtime-liquidsoap
index bfefcf46f..2b8384506 100755
--- a/python_apps/pypo/airtime-liquidsoap
+++ b/python_apps/pypo/airtime-liquidsoap
@@ -6,14 +6,12 @@ virtualenv_bin="/usr/lib/airtime/airtime_virtualenv/bin/"
ls_user="pypo"
export HOME="/var/tmp/airtime/pypo/"
api_client_path="/usr/lib/airtime/"
-ls_path="/usr/bin/airtime-liquidsoap --verbose -f"
+ls_path="/usr/bin/airtime-liquidsoap --verbose -f -d"
ls_param="/usr/lib/airtime/pypo/bin/liquidsoap_scripts/ls_script.liq"
exec 2>&1
export PYTHONPATH=${api_client_path}
-rm -f /etc/airtime/liquidsoap.cfg
-
cd /usr/lib/airtime/pypo/bin/liquidsoap_scripts
python generate_liquidsoap_cfg.py
diff --git a/python_apps/pypo/airtime-liquidsoap-init-d b/python_apps/pypo/airtime-liquidsoap-init-d
index 4180d5c67..37956a265 100755
--- a/python_apps/pypo/airtime-liquidsoap-init-d
+++ b/python_apps/pypo/airtime-liquidsoap-init-d
@@ -17,26 +17,32 @@ DAEMON=/usr/lib/airtime/pypo/bin/airtime-liquidsoap
PIDFILE=/var/run/airtime-liquidsoap.pid
start () {
- chown pypo:pypo /var/log/airtime/pypo
- chown pypo:pypo /var/log/airtime/pypo-liquidsoap
-
- start-stop-daemon --start --background --quiet --chuid $USERID:$GROUPID \
- --nicelevel -15 --make-pidfile --pidfile $PIDFILE --startas $DAEMON
-
+ start_no_monit
monit monitor airtime-liquidsoap >/dev/null 2>&1
}
stop () {
monit unmonitor airtime-liquidsoap >/dev/null 2>&1
- /usr/lib/airtime/airtime_virtualenv/bin/python /usr/lib/airtime/pypo/bin/liquidsoap_scripts/liquidsoap_prepare_terminate.py
-
+ #send term signal after 10 seconds
+ timeout -k 5 10 /usr/lib/airtime/airtime_virtualenv/bin/python \
+ /usr/lib/airtime/pypo/bin/liquidsoap_scripts/liquidsoap_prepare_terminate.py
# Send TERM after 5 seconds, wait at most 30 seconds.
- start-stop-daemon --stop --oknodo --retry 5 --quiet --pidfile $PIDFILE
+ start-stop-daemon --stop --oknodo --retry=TERM/10/KILL/5 --quiet --pidfile $PIDFILE
rm -f $PIDFILE
+ sleep 3
}
start_no_monit() {
- start-stop-daemon --start --background --quiet --chuid $USERID:$USERID --make-pidfile --pidfile $PIDFILE --startas $DAEMON
+ chown pypo:pypo /var/log/airtime/pypo
+ chown pypo:pypo /var/log/airtime/pypo-liquidsoap
+ chown pypo:pypo /etc/airtime/liquidsoap.cfg
+
+ rm -f $PIDFILE
+ touch $PIDFILE
+ chown pypo:pypo $PIDFILE
+
+ start-stop-daemon --start --quiet --chuid $USERID:$GROUPID \
+ --pidfile $PIDFILE --nicelevel -15 --startas $DAEMON
}
@@ -60,8 +66,8 @@ case "${1:-''}" in
;;
'status')
- if [ -f "/var/run/airtime-liquidsoap.pid" ]; then
- pid=`cat /var/run/airtime-liquidsoap.pid`
+ if [ -f "$PIDFILE" ]; then
+ pid=`cat $PIDFILE`
if [ -d "/proc/$pid" ]; then
echo "Liquidsoap is running"
exit 0
diff --git a/python_apps/pypo/airtime-playout b/python_apps/pypo/airtime-playout
index 56aa587cd..5521c91ed 100755
--- a/python_apps/pypo/airtime-playout
+++ b/python_apps/pypo/airtime-playout
@@ -3,14 +3,14 @@
virtualenv_bin="/usr/lib/airtime/airtime_virtualenv/bin/"
. ${virtualenv_bin}activate
-pypo_user="pypo"
+# Absolute path to this script
+SCRIPT=`readlink -f $0`
+# Absolute directory this script is in
+pypo_path=`dirname $SCRIPT`
-# Location of pypo_cli.py Python script
-pypo_path="/usr/lib/airtime/pypo/bin/"
api_client_path="/usr/lib/airtime/"
pypo_script="pypocli.py"
cd ${pypo_path}
-exec 2>&1
set +e
cat /etc/default/locale | grep -i "LANG=.*UTF-\?8"
@@ -26,6 +26,6 @@ export LC_ALL=`cat /etc/default/locale | grep "LANG=" | cut -d= -f2 | tr -d "\n\
export TERM=xterm
-exec python ${pypo_path}${pypo_script} > /var/log/airtime/pypo/py-interpreter.log 2>&1
+exec python ${pypo_path}/${pypo_script} > /var/log/airtime/pypo/py-interpreter.log 2>&1
# EOF
diff --git a/python_apps/pypo/eventtypes.py b/python_apps/pypo/eventtypes.py
new file mode 100644
index 000000000..5f9c871db
--- /dev/null
+++ b/python_apps/pypo/eventtypes.py
@@ -0,0 +1,6 @@
+FILE = "file"
+EVENT = "event"
+STREAM_BUFFER_START = "stream_buffer_start"
+STREAM_OUTPUT_START = "stream_output_start"
+STREAM_BUFFER_END = "stream_buffer_end"
+STREAM_OUTPUT_END = "stream_output_end"
diff --git a/python_apps/pypo/install/pypo-copy-files.py b/python_apps/pypo/install/pypo-copy-files.py
index ed7368b87..e88b46a64 100644
--- a/python_apps/pypo/install/pypo-copy-files.py
+++ b/python_apps/pypo/install/pypo-copy-files.py
@@ -6,6 +6,7 @@ import sys
import subprocess
import random
import string
+import re
from configobj import ConfigObj
if os.geteuid() != 0:
@@ -36,6 +37,30 @@ def get_rand_string(length=10):
def get_rand_string(length=10):
return ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(length))
+def get_monit_version():
+ version = 0
+ try:
+ p = subprocess.Popen(['monit', '-V'], stdout=subprocess.PIPE)
+ out = p.communicate()[0].strip()
+ search = re.search(r'This is Monit version (.*)\n', out, re.IGNORECASE)
+
+ if search:
+ matches = search.groups()
+ if len(matches) == 1:
+ version = matches[0]
+ except Exception:
+ print "Could not get monit version"
+
+ return version
+
+#return 1 if version1 > version2
+#return 0 if version1 == version2
+#return -1 if version1 < version2
+def version_compare(version1, version2):
+ def normalize(v):
+ return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
+ return cmp(normalize(version1), normalize(version2))
+
PATH_INI_FILE = '/etc/airtime/pypo.cfg'
try:
@@ -63,9 +88,15 @@ try:
#copy monit files
shutil.copy('%s/../../monit/monit-airtime-generic.cfg'%current_script_dir, '/etc/monit/conf.d/')
subprocess.call('sed -i "s/\$admin_pass/%s/g" /etc/monit/conf.d/monit-airtime-generic.cfg' % get_rand_string(), shell=True)
- shutil.copy('%s/../../monit/monit-airtime-rabbitmq-server.cfg'%current_script_dir, '/etc/monit/conf.d/')
- shutil.copy('%s/../monit-airtime-liquidsoap.cfg'%current_script_dir, '/etc/monit/conf.d/')
+ monit_version = get_monit_version()
+ if version_compare(monit_version, "5.3.0") >= 0:
+ shutil.copy('%s/../monit-airtime-liquidsoap.cfg' % current_script_dir, \
+ '/etc/monit/conf.d/monit-airtime-liquidsoap.cfg')
+ else:
+ shutil.copy('%s/../monit-pre530-airtime-liquidsoap.cfg' % current_script_dir, \
+ '/etc/monit/conf.d/monit-airtime-liquidsoap.cfg')
+
shutil.copy('%s/../monit-airtime-playout.cfg'%current_script_dir, '/etc/monit/conf.d/')
#create pypo log dir
diff --git a/python_apps/pypo/install/pypo-remove-files.py b/python_apps/pypo/install/pypo-remove-files.py
index 6b26d7484..3a20a2dda 100644
--- a/python_apps/pypo/install/pypo-remove-files.py
+++ b/python_apps/pypo/install/pypo-remove-files.py
@@ -49,7 +49,6 @@ try:
remove_file("/etc/monit/conf.d/monit-airtime-playout.cfg")
remove_file("/etc/monit/conf.d/monit-airtime-liquidsoap.cfg")
remove_file("/etc/monit/conf.d/monit-airtime-generic.cfg")
- remove_file("/etc/monit/conf.d/monit-airtime-rabbitmq-server.cfg")
except Exception, e:
print e
diff --git a/python_apps/pypo/install/pypo-uninitialize.py b/python_apps/pypo/install/pypo-uninitialize.py
index 8765e5a18..65ac91991 100644
--- a/python_apps/pypo/install/pypo-uninitialize.py
+++ b/python_apps/pypo/install/pypo-uninitialize.py
@@ -6,7 +6,7 @@ if os.geteuid() != 0:
print "Please run this as root."
sys.exit(1)
-try:
+try:
#stop pypo and liquidsoap processes
print "Waiting for Pypo process to stop...",
try:
@@ -18,12 +18,16 @@ try:
print "OK"
else:
print "Wasn't running"
-
+
print "Waiting for Liquidsoap process to stop...",
if (os.path.exists('/etc/init.d/airtime-liquidsoap')):
subprocess.call("invoke-rc.d airtime-liquidsoap stop", shell=True)
print "OK"
else:
print "Wasn't running"
+
+ subprocess.call("update-rc.d -f airtime-playout remove".split(" "))
+ subprocess.call("update-rc.d -f airtime-liquidsoap remove".split(" "))
+
except Exception, e:
print e
diff --git a/python_apps/pypo/install/pypo-uninstall.py b/python_apps/pypo/install/pypo-uninstall.py
deleted file mode 100644
index 7bc01ca4b..000000000
--- a/python_apps/pypo/install/pypo-uninstall.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-from configobj import ConfigObj
-
-if os.geteuid() != 0:
- print "Please run this as root."
- sys.exit(1)
-
-PATH_INI_FILE = '/etc/airtime/pypo.cfg'
-
-def remove_path(path):
- os.system('rm -rf "%s"' % path)
-
-def get_current_script_dir():
- current_script_dir = os.path.realpath(__file__)
- index = current_script_dir.rindex('/')
- return current_script_dir[0:index]
-
-def remove_monit_file():
- os.system("rm -f /etc/monit/conf.d/monit-airtime-playout.cfg")
- os.system("rm -f /etc/monit/conf.d/monit-airtime-liquidsoap.cfg")
-
-try:
- # load config file
- try:
- config = ConfigObj(PATH_INI_FILE)
- except Exception, e:
- print 'Error loading config file: ', e
- sys.exit(1)
-
- os.system("invoke-rc.d airtime-playout stop")
- os.system("invoke-rc.d airtime-liquidsoap stop")
-
- os.system("rm -f /etc/init.d/airtime-playout")
- os.system("rm -f /etc/init.d/airtime-liquidsoap")
-
- os.system("update-rc.d -f airtime-playout remove >/dev/null 2>&1")
- os.system("update-rc.d -f airtime-liquidsoap remove >/dev/null 2>&1")
-
- #remove logrotate script
- os.system("rm -f /etc/logrotate.d/airtime-liquidsoap")
-
- print "Removing monit file"
- remove_monit_file()
-
- print "Removing cache directories"
- remove_path(config["cache_base_dir"])
-
- print "Removing symlinks"
- os.system("rm -f /usr/bin/airtime-liquidsoap")
-
- print "Removing pypo files"
- remove_path(config["bin_dir"])
-
- print "Pypo uninstall complete."
-except Exception, e:
- print "exception:" + str(e)
diff --git a/python_apps/pypo/liquidsoap_scripts/generate_liquidsoap_cfg.py b/python_apps/pypo/liquidsoap_scripts/generate_liquidsoap_cfg.py
index 20d99976f..b123763e2 100644
--- a/python_apps/pypo/liquidsoap_scripts/generate_liquidsoap_cfg.py
+++ b/python_apps/pypo/liquidsoap_scripts/generate_liquidsoap_cfg.py
@@ -26,13 +26,10 @@ def generate_liquidsoap_config(ss):
logging.basicConfig(format='%(message)s')
ac = AirtimeApiClient(logging.getLogger())
-ss = ac.get_stream_setting()
-
-if ss is not None:
- try:
- generate_liquidsoap_config(ss)
- except Exception, e:
- logging.error(e)
-else:
+try:
+ ss = ac.get_stream_setting()
+ generate_liquidsoap_config(ss)
+except Exception, e:
+ logging.error(str(e))
print "Unable to connect to the Airtime server."
sys.exit(1)
diff --git a/python_apps/pypo/liquidsoap_scripts/liquidsoap_auth.py b/python_apps/pypo/liquidsoap_scripts/liquidsoap_auth.py
index 862231cc2..838898afb 100644
--- a/python_apps/pypo/liquidsoap_scripts/liquidsoap_auth.py
+++ b/python_apps/pypo/liquidsoap_scripts/liquidsoap_auth.py
@@ -15,4 +15,7 @@ elif dj_type == '--dj':
response = api_clients.check_live_stream_auth(username, password, source_type)
-print response['msg']
+if 'msg' in response:
+ print response['msg']
+else:
+ print False
diff --git a/python_apps/pypo/liquidsoap_scripts/liquidsoap_prepare_terminate.py b/python_apps/pypo/liquidsoap_scripts/liquidsoap_prepare_terminate.py
index e1dac82b6..2f632d9c7 100644
--- a/python_apps/pypo/liquidsoap_scripts/liquidsoap_prepare_terminate.py
+++ b/python_apps/pypo/liquidsoap_scripts/liquidsoap_prepare_terminate.py
@@ -6,14 +6,14 @@ try:
config = ConfigObj('/etc/airtime/pypo.cfg')
LS_HOST = config['ls_host']
LS_PORT = config['ls_port']
-
+
tn = telnetlib.Telnet(LS_HOST, LS_PORT)
tn.write("master_harbor.stop\n")
tn.write("live_dj_harbor.stop\n")
tn.write('exit\n')
tn.read_all()
-
+
except Exception, e:
print('Error loading config file: %s', e)
sys.exit()
-
\ No newline at end of file
+
diff --git a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq
index 3761f91c1..ccb37026f 100644
--- a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq
+++ b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq
@@ -1,6 +1,6 @@
def notify(m)
#current_media_id := string_of(m['schedule_table_id'])
- command = "/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --data='#{!pypo_data}' --media-id=#{m['schedule_table_id']} &"
+ command = "/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --media-id=#{m['schedule_table_id']} &"
log(command)
system(command)
end
@@ -78,14 +78,16 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de
source = ref s
def on_error(msg)
connected := "false"
- system("/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --error='#{msg}' --stream-id=#{stream} --time=#{!time} &")
- log("/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --error='#{msg}' --stream-id=#{stream} --time=#{!time} &")
+ command = "/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --error='#{msg}' --stream-id=#{stream} --time=#{!time} &"
+ system(command)
+ log(command)
5.
end
def on_connect()
connected := "true"
- system("/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --connect --stream-id=#{stream} --time=#{!time} &")
- log("/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --connect --stream-id=#{stream} --time=#{!time} &")
+ command = "/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --connect --stream-id=#{stream} --time=#{!time} &"
+ system(command)
+ log(command)
end
stereo = (channels == "stereo")
@@ -352,28 +354,32 @@ end
# Add a skip function to a source
# when it does not have one
# by default
-def add_skip_command(s)
- # A command to skip
- def skip(_)
- # get playing (active) queue and flush it
- l = list.hd(server.execute("queue.secondary_queue"))
- l = string.split(separator=" ",l)
- list.iter(fun (rid) -> ignore(server.execute("queue.remove #{rid}")), l)
+#def add_skip_command(s)
+# # A command to skip
+# def skip(_)
+# # get playing (active) queue and flush it
+# l = list.hd(server.execute("queue.secondary_queue"))
+# l = string.split(separator=" ",l)
+# list.iter(fun (rid) -> ignore(server.execute("queue.remove #{rid}")), l)
+#
+# l = list.hd(server.execute("queue.primary_queue"))
+# l = string.split(separator=" ", l)
+# if list.length(l) > 0 then
+# source.skip(s)
+# "Skipped"
+# else
+# "Not skipped"
+# end
+# end
+# # Register the command:
+# server.register(namespace="source",
+# usage="skip",
+# description="Skip the current song.",
+# "skip",fun(s) -> begin log("source.skip") skip(s) end)
+#end
- l = list.hd(server.execute("queue.primary_queue"))
- l = string.split(separator=" ", l)
- if list.length(l) > 0 then
- source.skip(s)
- "Skipped"
- else
- "Not skipped"
- end
- end
- # Register the command:
- server.register(namespace="source",
- usage="skip",
- description="Skip the current song.",
- "skip",fun(s) -> begin log("source.skip") skip(s) end)
+def clear_queue(s)
+ source.skip(s)
end
def set_dynamic_source_id(id) =
diff --git a/python_apps/pypo/liquidsoap_scripts/ls_script.liq b/python_apps/pypo/liquidsoap_scripts/ls_script.liq
index cd01ad453..1a7981318 100644
--- a/python_apps/pypo/liquidsoap_scripts/ls_script.liq
+++ b/python_apps/pypo/liquidsoap_scripts/ls_script.liq
@@ -1,10 +1,12 @@
-%include "library/pervasives.liq"
%include "/etc/airtime/liquidsoap.cfg"
set("log.file.path", log_file)
set("log.stdout", true)
set("server.telnet", true)
set("server.telnet.port", 1234)
+set("init.daemon.pidfile.path", "/var/run/airtime-liquidsoap.pid")
+
+%include "library/pervasives.liq"
#Dynamic source list
#dyn_sources = ref []
@@ -31,24 +33,49 @@ s2_namespace = ref ''
s3_namespace = ref ''
just_switched = ref false
-#stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20'))
-
%include "ls_lib.liq"
-queue = audio_to_stereo(id="queue_src", request.equeue(id="queue", length=0.5))
-queue = cue_cut(queue)
+sources = ref []
+source_id = ref 0
+
+def create_source()
+ l = request.equeue(id="s#{!source_id}", length=0.5)
+ l = cue_cut(l)
+ sources := list.append([l], !sources)
+ server.register(namespace="queues",
+ "s#{!source_id}_skip",
+ fun (s) -> begin log("queues.s#{!source_id}_skip")
+ clear_queue(l)
+ "Done"
+ end)
+ source_id := !source_id + 1
+end
+
+create_source()
+create_source()
+create_source()
+create_source()
+
+create_source()
+create_source()
+create_source()
+create_source()
+
+queue = add(!sources)
+
+queue = audio_to_stereo(id="queue_src", queue)
queue = amplify(1., override="replay_gain", queue)
# the crossfade function controls fade in/out
queue = crossfade_airtime(queue)
queue = on_metadata(notify, queue)
-queue = map_metadata(update=false, append_title, queue)
output.dummy(fallible=true, queue)
http = input.http_restart(id="http")
http = cross_http(http_input_id="http",http)
output.dummy(fallible=true, http)
stream_queue = http_fallback(http_input_id="http", http=http, default=queue)
+stream_queue = map_metadata(update=false, append_title, stream_queue)
ignore(output.dummy(stream_queue, fallible=true))
@@ -78,8 +105,8 @@ server.register(namespace="dynamic_source",
description="Enable webstream output",
usage='start',
"output_start",
- fun (s) -> begin log("dynamic_source.output_start")
- notify([("schedule_table_id", !current_dyn_id)])
+ fun (s) -> begin log("dynamic_source.output_start")
+ notify([("schedule_table_id", !current_dyn_id)])
webstream_enabled := true "enabled" end)
server.register(namespace="dynamic_source",
description="Enable webstream output",
@@ -154,8 +181,9 @@ def make_scheduled_play_unavailable()
end
def update_source_status(sourcename, status) =
- system("/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --source-name=#{sourcename} --source-status=#{status} &")
- log("/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --source-name=#{sourcename} --source-status=#{status} &")
+ command = "/usr/lib/airtime/pypo/bin/liquidsoap_scripts/notify.sh --source-name=#{sourcename} --source-status=#{status} &"
+ system(command)
+ log(command)
end
def live_dj_connect(header) =
@@ -195,43 +223,59 @@ def check_dj_client(user,password) =
hd == "True"
end
-def append_dj_inputs(master_harbor_input_port, master_harbor_input_mount_point, dj_harbor_input_port, dj_harbor_input_mount_point, s) =
- if master_harbor_input_port != 0 and master_harbor_input_mount_point != "" and dj_harbor_input_port != 0 and dj_harbor_input_mount_point != "" then
- master_dj = mksafe(audio_to_stereo(input.harbor(id="master_harbor", master_harbor_input_mount_point, port=master_harbor_input_port, auth=check_master_dj_client,
- max=40., on_connect=master_dj_connect, on_disconnect=master_dj_disconnect)))
- dj_live = mksafe(audio_to_stereo(input.harbor(id="live_dj_harbor", dj_harbor_input_mount_point, port=dj_harbor_input_port, auth=check_dj_client,
- max=40., on_connect=live_dj_connect, on_disconnect=live_dj_disconnect)))
+s = switch(id="schedule_noise_switch",
+ track_sensitive=false,
+ transitions=[transition_default, transition],
+ [({!scheduled_play_enabled}, stream_queue), ({true}, default)]
+ )
- ignore(output.dummy(master_dj, fallible=true))
- ignore(output.dummy(dj_live, fallible=true))
- switch(id="master_dj_switch", track_sensitive=false, transitions=[transition, transition, transition], [({!master_dj_enabled},master_dj), ({!live_dj_enabled},dj_live), ({true}, s)])
- elsif master_harbor_input_port != 0 and master_harbor_input_mount_point != "" then
- master_dj = mksafe(audio_to_stereo(input.harbor(id="master_harbor", master_harbor_input_mount_point, port=master_harbor_input_port, auth=check_master_dj_client,
- max=40., on_connect=master_dj_connect, on_disconnect=master_dj_disconnect)))
- ignore(output.dummy(master_dj, fallible=true))
-
- switch(id="master_dj_switch", track_sensitive=false, transitions=[transition, transition], [({!master_dj_enabled},master_dj), ({true}, s)])
- elsif dj_harbor_input_port != 0 and dj_harbor_input_mount_point != "" then
- dj_live = mksafe(audio_to_stereo(input.harbor(id="live_dj_harbor", dj_harbor_input_mount_point, port=dj_harbor_input_port, auth=check_dj_client,
- max=40., on_connect=live_dj_connect, on_disconnect=live_dj_disconnect)))
+s = if dj_live_stream_port != 0 and dj_live_stream_mp != "" then
+ dj_live = mksafe(
+ audio_to_stereo(
+ input.harbor(id="live_dj_harbor",
+ dj_live_stream_mp,
+ port=dj_live_stream_port,
+ auth=check_dj_client,
+ max=40.,
+ on_connect=live_dj_connect,
+ on_disconnect=live_dj_disconnect)))
- ignore(output.dummy(dj_live, fallible=true))
- switch(id="live_dj_switch", track_sensitive=false, transitions=[transition, transition], [({!live_dj_enabled},dj_live), ({true}, s)])
- else
- s
- end
+ ignore(output.dummy(dj_live, fallible=true))
+
+ switch(id="show_schedule_noise_switch",
+ track_sensitive=false,
+ transitions=[transition, transition],
+ [({!live_dj_enabled}, dj_live), ({true}, s)]
+ )
+else
+ s
end
-s = switch(id="default_switch", track_sensitive=false,
- transitions=[transition_default, transition],
- [({!scheduled_play_enabled}, stream_queue),({true},default)])
+s = if master_live_stream_port != 0 and master_live_stream_mp != "" then
+ master_dj = mksafe(
+ audio_to_stereo(
+ input.harbor(id="master_harbor",
+ master_live_stream_mp,
+ port=master_live_stream_port,
+ auth=check_master_dj_client,
+ max=40.,
+ on_connect=master_dj_connect,
+ on_disconnect=master_dj_disconnect)))
+
+ ignore(output.dummy(master_dj, fallible=true))
+
+ switch(id="master_show_schedule_noise_switch",
+ track_sensitive=false,
+ transitions=[transition, transition],
+ [({!master_dj_enabled}, master_dj), ({true}, s)]
+ )
+else
+ s
+end
-s = append_dj_inputs(master_live_stream_port, master_live_stream_mp,
- dj_live_stream_port, dj_live_stream_mp, s)
# Attach a skip command to the source s:
-
-add_skip_command(s)
+#add_skip_command(s)
server.register(namespace="streams",
description="Stop Master DJ source.",
diff --git a/python_apps/pypo/listenerstat.py b/python_apps/pypo/listenerstat.py
index e6a272703..1a7bb27b1 100644
--- a/python_apps/pypo/listenerstat.py
+++ b/python_apps/pypo/listenerstat.py
@@ -102,8 +102,10 @@ class ListenerStat(Thread):
stats.append(self.get_shoutcast_stats(v))
self.update_listener_stat_error(v["mount"], 'OK')
except Exception, e:
- self.logger.error('Exception: %s', e)
- self.update_listener_stat_error(v["mount"], str(e))
+ try:
+ self.update_listener_stat_error(v["mount"], str(e))
+ except Exception, e:
+ self.logger.error('Exception: %s', e)
return stats
@@ -123,13 +125,9 @@ class ListenerStat(Thread):
while True:
try:
stream_parameters = self.get_stream_parameters()
-
stats = self.get_stream_stats(stream_parameters["stream_params"])
- self.logger.debug(stats)
- if not stats:
- self.logger.error("Not able to get listener stats")
- else:
+ if stats:
self.push_stream_stats(stats)
except Exception, e:
self.logger.error('Exception: %s', e)
diff --git a/python_apps/pypo/media/update/replaygain.py b/python_apps/pypo/media/update/replaygain.py
index 5af7cd4a1..329a1e6a0 100644
--- a/python_apps/pypo/media/update/replaygain.py
+++ b/python_apps/pypo/media/update/replaygain.py
@@ -14,14 +14,14 @@ def get_process_output(command):
Run subprocess and return stdout
"""
logger.debug(command)
- p = Popen(command, shell=True, stdout=PIPE)
+ p = Popen(command, stdout=PIPE, stderr=PIPE)
return p.communicate()[0].strip()
def run_process(command):
"""
Run subprocess and return "return code"
"""
- p = Popen(command, shell=True)
+ p = Popen(command, stdout=PIPE, stderr=PIPE)
return os.waitpid(p.pid, 0)[1]
def get_mime_type(file_path):
@@ -31,7 +31,8 @@ def get_mime_type(file_path):
for files which do not have a mp3/ogg/flac extension.
"""
- return get_process_output("timeout 5 file -b --mime-type %s" % file_path)
+ command = ['timeout', '5', 'file', '-b', '--mime-type', file_path]
+ return get_process_output(command)
def duplicate_file(file_path):
"""
@@ -89,41 +90,37 @@ def calculate_replay_gain(file_path):
temp_file_path = duplicate_file(file_path)
file_type = get_file_type(file_path)
- nice_level = '15'
+ nice_level = '19'
if file_type:
if file_type == 'mp3':
- if run_process("which mp3gain > /dev/null") == 0:
- command = 'nice -n %s mp3gain -q "%s" 2> /dev/null' \
- % (nice_level, temp_file_path)
+ if run_process(['which', 'mp3gain']) == 0:
+ command = ['nice', '-n', nice_level, 'mp3gain', '-q', temp_file_path]
out = get_process_output(command)
search = re.search(r'Recommended "Track" dB change: (.*)', \
out)
else:
logger.warn("mp3gain not found")
elif file_type == 'vorbis':
- command = "which vorbisgain > /dev/null && which ogginfo > \
- /dev/null"
- if run_process(command) == 0:
- command = 'nice -n %s vorbisgain -q -f "%s" 2>/dev/null \
- >/dev/null' % (nice_level,temp_file_path)
+ if run_process(['which', 'ogginfo']) == 0 and \
+ run_process(['which', 'vorbisgain']) == 0:
+ command = ['nice', '-n', nice_level, 'vorbisgain', '-q', '-f', temp_file_path]
run_process(command)
- out = get_process_output('ogginfo "%s"' % temp_file_path)
+ out = get_process_output(['ogginfo', temp_file_path])
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else:
logger.warn("vorbisgain/ogginfo not found")
elif file_type == 'flac':
- if run_process("which metaflac > /dev/null") == 0:
+ if run_process(['which', 'metaflac']) == 0:
- command = 'nice -n %s metaflac --add-replay-gain "%s"' \
- % (nice_level, temp_file_path)
+ command = ['nice', '-n', nice_level, 'metaflac', \
+ '--add-replay-gain', temp_file_path]
run_process(command)
- command = 'nice -n %s metaflac \
- --show-tag=REPLAYGAIN_TRACK_GAIN "%s"' \
- % (nice_level, temp_file_path)
-
+ command = ['nice', '-n', nice_level, 'metaflac', \
+ '--show-tag=REPLAYGAIN_TRACK_GAIN', \
+ temp_file_path]
out = get_process_output(command)
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else: logger.warn("metaflac not found")
diff --git a/python_apps/pypo/media/update/replaygainupdater.py b/python_apps/pypo/media/update/replaygainupdater.py
index 2f52c0a23..5466e30ce 100644
--- a/python_apps/pypo/media/update/replaygainupdater.py
+++ b/python_apps/pypo/media/update/replaygainupdater.py
@@ -55,10 +55,13 @@ class ReplayGainUpdater(Thread):
for f in files:
full_path = os.path.join(dir_path, f['fp'])
processed_data.append((f['id'], replaygain.calculate_replay_gain(full_path)))
+ total += 1
try:
self.api_client.update_replay_gain_values(processed_data)
- except Exception as e: self.unexpected_exception(e)
+ except Exception as e:
+ self.logger.error(e)
+ self.logger.debug(traceback.format_exc())
if len(files) == 0: break
self.logger.info("Processed: %d songs" % total)
@@ -67,15 +70,15 @@ class ReplayGainUpdater(Thread):
self.logger.error(e)
self.logger.debug(traceback.format_exc())
def run(self):
- try:
- while True:
- self.logger.info("Runnning replaygain updater")
+ while True:
+ try:
+ self.logger.info("Running replaygain updater")
self.main()
# Sleep for 5 minutes in case new files have been added
- time.sleep(60 * 5)
- except Exception, e:
- self.logger.error('ReplayGainUpdater Exception: %s', traceback.format_exc())
- self.logger.error(e)
+ except Exception, e:
+ self.logger.error('ReplayGainUpdater Exception: %s', traceback.format_exc())
+ self.logger.error(e)
+ time.sleep(60 * 5)
if __name__ == "__main__":
rgu = ReplayGainUpdater()
diff --git a/python_apps/pypo/media/update/silananalyzer.py b/python_apps/pypo/media/update/silananalyzer.py
new file mode 100644
index 000000000..4d93deddf
--- /dev/null
+++ b/python_apps/pypo/media/update/silananalyzer.py
@@ -0,0 +1,88 @@
+from threading import Thread
+
+import traceback
+import time
+import subprocess
+import json
+
+
+class SilanAnalyzer(Thread):
+ """
+ The purpose of the class is to query the server for a list of files which
+ do not have a Silan value calculated. This class will iterate over the
+ list calculate the values, update the server and repeat the process until
+ the server reports there are no files left.
+ """
+
+ @staticmethod
+ def start_silan(apc, logger):
+ me = SilanAnalyzer(apc, logger)
+ me.start()
+
+ def __init__(self, apc, logger):
+ Thread.__init__(self)
+ self.api_client = apc
+ self.logger = logger
+
+ def main(self):
+ while True:
+ # keep getting few rows at a time for current music_dir (stor
+ # or watched folder).
+ total = 0
+
+ # return a list of pairs where the first value is the
+ # file's database row id and the second value is the
+ # filepath
+ files = self.api_client.get_files_without_silan_value()
+ total_files = len(files)
+ if total_files == 0: return
+ processed_data = []
+ for f in files:
+ full_path = f['fp']
+ # silence detect(set default queue in and out)
+ try:
+ data = {}
+ command = ['nice', '-n', '19', 'silan', '-b', '-f', 'JSON', full_path]
+ try:
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE)
+ out = proc.communicate()[0].strip('\r\n')
+ info = json.loads(out)
+ data['cuein'] = str('{0:f}'.format(info['sound'][0][0]))
+ data['cueout'] = str('{0:f}'.format(info['sound'][-1][1]))
+ except Exception, e:
+ self.logger.error(str(command))
+ self.logger.error(e)
+ processed_data.append((f['id'], data))
+ total += 1
+ if total % 5 == 0:
+ self.logger.info("Total %s / %s files has been processed.." % (total, total_files))
+ except Exception, e:
+ self.logger.error(e)
+ self.logger.error(traceback.format_exc())
+
+ try:
+ self.api_client.update_cue_values_by_silan(processed_data)
+ except Exception ,e:
+ self.logger.error(e)
+ self.logger.error(traceback.format_exc())
+
+ self.logger.info("Processed: %d songs" % total)
+
+ def run(self):
+ while True:
+ try:
+ self.logger.info("Running Silan analyzer")
+ self.main()
+ except Exception, e:
+ self.logger.error('Silan Analyzer Exception: %s', traceback.format_exc())
+ self.logger.error(e)
+ self.logger.info("Sleeping for 5...")
+ time.sleep(60 * 5)
+
+if __name__ == "__main__":
+ from api_clients import api_client
+ import logging
+ logging.basicConfig(level=logging.DEBUG)
+ api_client = api_client.AirtimeApiClient()
+ SilanAnalyzer.start_silan(api_client, logging)
+
diff --git a/python_apps/pypo/monit-airtime-liquidsoap.cfg b/python_apps/pypo/monit-airtime-liquidsoap.cfg
index f8efcaf18..3b674aec2 100644
--- a/python_apps/pypo/monit-airtime-liquidsoap.cfg
+++ b/python_apps/pypo/monit-airtime-liquidsoap.cfg
@@ -1,4 +1,4 @@
- set daemon 10 # Poll at 5 second intervals
+ set daemon 15 # Poll at 5 second intervals
set logfile /var/log/monit.log
set httpd port 2812
@@ -7,3 +7,10 @@
with pidfile "/var/run/airtime-liquidsoap.pid"
start program = "/etc/init.d/airtime-liquidsoap start" with timeout 5 seconds
stop program = "/etc/init.d/airtime-liquidsoap stop"
+
+ if mem > 600 MB for 3 cycles then restart
+ if failed host localhost port 1234
+ send "version\r\nexit\r\n"
+ expect "Liquidsoap"
+ with timeout 2 seconds retry 3 for 2 cycles
+ then restart
diff --git a/python_apps/pypo/monit-airtime-playout.cfg b/python_apps/pypo/monit-airtime-playout.cfg
index 5b096c72a..453f4efec 100644
--- a/python_apps/pypo/monit-airtime-playout.cfg
+++ b/python_apps/pypo/monit-airtime-playout.cfg
@@ -5,5 +5,5 @@
check process airtime-playout
with pidfile "/var/run/airtime-playout.pid"
- start program = "/etc/init.d/airtime-playout monit-restart" with timeout 5 seconds
+ start program = "/etc/init.d/airtime-playout start" with timeout 5 seconds
stop program = "/etc/init.d/airtime-playout stop"
diff --git a/python_apps/pypo/monit-pre530-airtime-liquidsoap.cfg b/python_apps/pypo/monit-pre530-airtime-liquidsoap.cfg
new file mode 100644
index 000000000..972edd46f
--- /dev/null
+++ b/python_apps/pypo/monit-pre530-airtime-liquidsoap.cfg
@@ -0,0 +1,9 @@
+ set daemon 15 # Poll at 5 second intervals
+ set logfile /var/log/monit.log
+
+ set httpd port 2812
+
+ check process airtime-liquidsoap
+ with pidfile "/var/run/airtime-liquidsoap.pid"
+ start program = "/etc/init.d/airtime-liquidsoap start" with timeout 5 seconds
+ stop program = "/etc/init.d/airtime-liquidsoap stop"
diff --git a/python_apps/pypo/pypocli.py b/python_apps/pypo/pypocli.py
index ea8950d41..de30d65dd 100644
--- a/python_apps/pypo/pypocli.py
+++ b/python_apps/pypo/pypocli.py
@@ -13,8 +13,8 @@ import signal
import logging
import locale
import os
-from Queue import Queue
+from Queue import Queue
from threading import Lock
from pypopush import PypoPush
@@ -25,6 +25,7 @@ from listenerstat import ListenerStat
from pypomessagehandler import PypoMessageHandler
from media.update.replaygainupdater import ReplayGainUpdater
+from media.update.silananalyzer import SilanAnalyzer
from configobj import ConfigObj
@@ -62,7 +63,7 @@ try:
LogWriter.override_std_err(logger)
except Exception, e:
print "Couldn't configure logging"
- sys.exit()
+ sys.exit(1)
def configure_locale():
logger.debug("Before %s", locale.nl_langinfo(locale.CODESET))
@@ -126,21 +127,21 @@ def keyboardInterruptHandler(signum, frame):
def liquidsoap_running_test(telnet_lock, host, port, logger):
logger.debug("Checking to see if Liquidsoap is running")
- success = True
try:
telnet_lock.acquire()
tn = telnetlib.Telnet(host, port)
msg = "version\n"
tn.write(msg)
tn.write("exit\n")
- logger.info("Found: %s", tn.read_all())
+ response = tn.read_all()
+ logger.info("Found: %s", response)
except Exception, e:
logger.error(str(e))
- success = False
+ return False
finally:
telnet_lock.release()
- return success
+ return "Liquidsoap" in response
if __name__ == '__main__':
@@ -176,10 +177,18 @@ if __name__ == '__main__':
sys.exit()
api_client = api_client.AirtimeApiClient()
-
- ReplayGainUpdater.start_reply_gain(api_client)
- api_client.register_component("pypo")
+ ReplayGainUpdater.start_reply_gain(api_client)
+ SilanAnalyzer.start_silan(api_client, logger)
+
+ success = False
+ while not success:
+ try:
+ api_client.register_component('pypo')
+ success = True
+ except Exception, e:
+ logger.error(str(e))
+ time.sleep(10)
pypoFetch_q = Queue()
recorder_q = Queue()
@@ -220,7 +229,7 @@ if __name__ == '__main__':
stat.start()
# all join() are commented out because we want to exit entire pypo
- # if pypofetch is exiting
+ # if pypofetch terminates
#pmh.join()
#recorder.join()
#pp.join()
diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py
index ec3ef11ce..17d5e5898 100644
--- a/python_apps/pypo/pypofetch.py
+++ b/python_apps/pypo/pypofetch.py
@@ -7,21 +7,30 @@ import logging.config
import json
import telnetlib
import copy
-from threading import Thread
import subprocess
+import signal
+from datetime import datetime
from Queue import Empty
+from threading import Thread
+from subprocess import Popen, PIPE
+from configobj import ConfigObj
from api_clients import api_client
from std_err_override import LogWriter
-from configobj import ConfigObj
-
# configure logging
-logging.config.fileConfig("logging.cfg")
+logging_cfg = os.path.join(os.path.dirname(__file__), "logging.cfg")
+logging.config.fileConfig(logging_cfg)
logger = logging.getLogger()
LogWriter.override_std_err(logger)
+def keyboardInterruptHandler(signum, frame):
+ logger = logging.getLogger()
+ logger.info('\nKeyboard Interrupt\n')
+ sys.exit(0)
+signal.signal(signal.SIGINT, keyboardInterruptHandler)
+
#need to wait for Python 2.7 for this..
#logging.captureWarnings(True)
@@ -32,8 +41,6 @@ try:
LS_PORT = config['ls_port']
#POLL_INTERVAL = int(config['poll_interval'])
POLL_INTERVAL = 1800
-
-
except Exception, e:
logger.error('Error loading config file: %s', e)
sys.exit()
@@ -132,9 +139,10 @@ class PypoFetch(Thread):
elif(sourcename == "live_dj"):
command += "live_dj_harbor.kick\n"
- lock.acquire()
try:
+ lock.acquire()
tn = telnetlib.Telnet(LS_HOST, LS_PORT)
+ logger.info(command)
tn.write(command)
tn.write('exit\n')
tn.read_all()
@@ -143,6 +151,24 @@ class PypoFetch(Thread):
finally:
lock.release()
+ @staticmethod
+ def telnet_send(logger, lock, commands):
+ try:
+ lock.acquire()
+
+ tn = telnetlib.Telnet(LS_HOST, LS_PORT)
+ for i in commands:
+ logger.info(i)
+ tn.write(i)
+
+ tn.write('exit\n')
+ tn.read_all()
+ except Exception, e:
+ logger.error(str(e))
+ finally:
+ lock.release()
+
+
@staticmethod
def switch_source(logger, lock, sourcename, status):
logger.debug('Switching source: %s to "%s" status', sourcename, status)
@@ -159,16 +185,26 @@ class PypoFetch(Thread):
else:
command += "stop\n"
- lock.acquire()
- try:
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
- tn.write(command)
- tn.write('exit\n')
- tn.read_all()
- except Exception, e:
- logger.error(str(e))
- finally:
- lock.release()
+ PypoFetch.telnet_send(logger, lock, [command])
+
+
+ #TODO: Merge this with switch_source
+ def switch_source_temp(self, sourcename, status):
+ self.logger.debug('Switching source: %s to "%s" status', sourcename, status)
+ command = "streams."
+ if sourcename == "master_dj":
+ command += "master_dj_"
+ elif sourcename == "live_dj":
+ command += "live_dj_"
+ elif sourcename == "scheduled_play":
+ command += "scheduled_play_"
+
+ if status == "on":
+ command += "start\n"
+ else:
+ command += "stop\n"
+
+ return command
"""
grabs some information that are needed to be set on bootstrap time
@@ -176,22 +212,30 @@ class PypoFetch(Thread):
"""
def set_bootstrap_variables(self):
self.logger.debug('Getting information needed on bootstrap from Airtime')
- info = self.api_client.get_bootstrap_info()
- if info is None:
+ try:
+ info = self.api_client.get_bootstrap_info()
+ except Exception, e:
self.logger.error('Unable to get bootstrap info.. Exiting pypo...')
- sys.exit(1)
- else:
- self.logger.debug('info:%s', info)
- for k, v in info['switch_status'].iteritems():
- self.switch_source(self.logger, self.telnet_lock, k, v)
- self.update_liquidsoap_stream_format(info['stream_label'])
- self.update_liquidsoap_station_name(info['station_name'])
- self.update_liquidsoap_transition_fade(info['transition_fade'])
+ self.logger.error(str(e))
+
+ self.logger.debug('info:%s', info)
+ commands = []
+ for k, v in info['switch_status'].iteritems():
+ commands.append(self.switch_source_temp(k, v))
+
+ stream_format = info['stream_label']
+ station_name = info['station_name']
+ fade = info['transition_fade']
+
+ commands.append(('vars.stream_metadata_type %s\n' % stream_format).encode('utf-8'))
+ commands.append(('vars.station_name %s\n' % station_name).encode('utf-8'))
+ commands.append(('vars.default_dj_fade %s\n' % fade).encode('utf-8'))
+ PypoFetch.telnet_send(self.logger, self.telnet_lock, commands)
def restart_liquidsoap(self):
- self.telnet_lock.acquire()
try:
+ self.telnet_lock.acquire()
self.logger.info("Restarting Liquidsoap")
subprocess.call('/etc/init.d/airtime-liquidsoap restart', shell=True)
@@ -217,7 +261,7 @@ class PypoFetch(Thread):
self.set_bootstrap_variables()
#get the most up to date schedule, which will #initiate the process
#of making sure Liquidsoap is playing the schedule
- self.manual_schedule_fetch()
+ self.persistent_manual_schedule_fetch(max_attempts=5)
except Exception, e:
self.logger.error(str(e))
@@ -322,16 +366,21 @@ class PypoFetch(Thread):
This function updates the bootup time variable in Liquidsoap script
"""
- self.telnet_lock.acquire()
try:
+ self.telnet_lock.acquire()
tn = telnetlib.Telnet(LS_HOST, LS_PORT)
# update the boot up time of Liquidsoap. Since Liquidsoap is not restarting,
# we are manually adjusting the bootup time variable so the status msg will get
# updated.
current_time = time.time()
boot_up_time_command = "vars.bootup_time " + str(current_time) + "\n"
+ self.logger.info(boot_up_time_command)
tn.write(boot_up_time_command)
- tn.write("streams.connection_status\n")
+
+ connection_status = "streams.connection_status\n"
+ self.logger.info(connection_status)
+ tn.write(connection_status)
+
tn.write('exit\n')
output = tn.read_all()
@@ -356,6 +405,7 @@ class PypoFetch(Thread):
if(status == "true"):
self.api_client.notify_liquidsoap_status("OK", stream_id, str(fake_time))
+
def update_liquidsoap_stream_format(self, stream_format):
# Push stream metadata to liquidsoap
# TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!!
@@ -395,8 +445,8 @@ class PypoFetch(Thread):
self.logger.info(LS_HOST)
self.logger.info(LS_PORT)
- self.telnet_lock.acquire()
try:
+ self.telnet_lock.acquire()
tn = telnetlib.Telnet(LS_HOST, LS_PORT)
command = ('vars.station_name %s\n' % station_name).encode('utf-8')
self.logger.info(command)
@@ -436,6 +486,7 @@ class PypoFetch(Thread):
except Exception, e:
pass
+ media_copy = {}
for key in media:
media_item = media[key]
if (media_item['type'] == 'file'):
@@ -445,18 +496,29 @@ class PypoFetch(Thread):
media_item['file_ready'] = False
media_filtered[key] = media_item
+ media_item['start'] = datetime.strptime(media_item['start'], "%Y-%m-%d-%H-%M-%S")
+ media_item['end'] = datetime.strptime(media_item['end'], "%Y-%m-%d-%H-%M-%S")
+ media_copy[media_item['start']] = media_item
+
+
self.media_prepare_queue.put(copy.copy(media_filtered))
except Exception, e: self.logger.error("%s", e)
# Send the data to pypo-push
self.logger.debug("Pushing to pypo-push")
- self.push_queue.put(media)
+ self.push_queue.put(media_copy)
# cleanup
try: self.cache_cleanup(media)
except Exception, e: self.logger.error("%s", e)
+ def is_file_opened(self, path):
+ #Capture stderr to avoid polluting py-interpreter.log
+ proc = Popen(["lsof", path], stdout=PIPE, stderr=PIPE)
+ out = proc.communicate()[0].strip()
+ return bool(out)
+
def cache_cleanup(self, media):
"""
Get list of all files in the cache dir and remove them if they aren't being used anymore.
@@ -477,8 +539,14 @@ class PypoFetch(Thread):
self.logger.debug("Files to remove " + str(expired_files))
for f in expired_files:
try:
- self.logger.debug("Removing %s" % os.path.join(self.cache_dir, f))
- os.remove(os.path.join(self.cache_dir, f))
+ path = os.path.join(self.cache_dir, f)
+ self.logger.debug("Removing %s" % path)
+
+ #check if this file is opened (sometimes Liquidsoap is still
+ #playing the file due to our knowledge of the track length
+ #being incorrect!)
+ if not self.is_file_opened(path):
+ os.remove(path)
except Exception, e:
self.logger.error(e)
@@ -488,10 +556,20 @@ class PypoFetch(Thread):
self.process_schedule(self.schedule_data)
return success
+ def persistent_manual_schedule_fetch(self, max_attempts=1):
+ success = False
+ num_attempts = 0
+ while not success and num_attempts < max_attempts:
+ success = self.manual_schedule_fetch()
+ num_attempts += 1
+
+ return success
+
+
def main(self):
# Bootstrap: since we are just starting up, we need to grab the
# most recent schedule. After that we can just wait for updates.
- success = self.manual_schedule_fetch()
+ success = self.persistent_manual_schedule_fetch(max_attempts=5)
if success:
self.logger.info("Bootstrap schedule received: %s", self.schedule_data)
self.set_bootstrap_variables()
@@ -519,7 +597,7 @@ class PypoFetch(Thread):
self.handle_message(message)
except Empty, e:
self.logger.info("Queue timeout. Fetching schedule manually")
- self.manual_schedule_fetch()
+ self.persistent_manual_schedule_fetch(max_attempts=5)
except Exception, e:
import traceback
top = traceback.format_exc()
diff --git a/python_apps/pypo/pypoliqqueue.py b/python_apps/pypo/pypoliqqueue.py
new file mode 100644
index 000000000..439255704
--- /dev/null
+++ b/python_apps/pypo/pypoliqqueue.py
@@ -0,0 +1,87 @@
+from threading import Thread
+from collections import deque
+from datetime import datetime
+
+import traceback
+import sys
+import time
+
+
+from Queue import Empty
+
+import signal
+def keyboardInterruptHandler(signum, frame):
+ logger = logging.getLogger()
+ logger.info('\nKeyboard Interrupt\n')
+ sys.exit(0)
+signal.signal(signal.SIGINT, keyboardInterruptHandler)
+
+class PypoLiqQueue(Thread):
+ def __init__(self, q, pypo_liquidsoap, logger):
+ Thread.__init__(self)
+ self.queue = q
+ self.logger = logger
+ self.pypo_liquidsoap = pypo_liquidsoap
+
+ def main(self):
+ time_until_next_play = None
+ schedule_deque = deque()
+ media_schedule = None
+
+ while True:
+ try:
+ if time_until_next_play is None:
+ self.logger.info("waiting indefinitely for schedule")
+ media_schedule = self.queue.get(block=True)
+ else:
+ self.logger.info("waiting %ss until next scheduled item" % \
+ time_until_next_play)
+ media_schedule = self.queue.get(block=True, \
+ timeout=time_until_next_play)
+ except Empty, e:
+ #Time to push a scheduled item.
+ media_item = schedule_deque.popleft()
+ self.pypo_liquidsoap.push_item(media_item)
+ if len(schedule_deque):
+ time_until_next_play = \
+ self.date_interval_to_seconds(
+ schedule_deque[0]['start'] - datetime.utcnow())
+ if time_until_next_play < 0:
+ time_until_next_play = 0
+ else:
+ time_until_next_play = None
+ else:
+ self.logger.info("New schedule received: %s", media_schedule)
+
+ #new schedule received. Replace old one with this.
+ schedule_deque.clear()
+
+ keys = sorted(media_schedule.keys())
+ for i in keys:
+ schedule_deque.append(media_schedule[i])
+
+ if len(keys):
+ time_until_next_play = self.date_interval_to_seconds(\
+ keys[0] - datetime.utcnow())
+ else:
+ time_until_next_play = None
+
+
+ def date_interval_to_seconds(self, interval):
+ """
+ Convert timedelta object into int representing the number of seconds. If
+ number of seconds is less than 0, then return 0.
+ """
+ seconds = (interval.microseconds + \
+ (interval.seconds + interval.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
+ if seconds < 0: seconds = 0
+
+ return seconds
+
+ def run(self):
+ try: self.main()
+ except Exception, e:
+ self.logger.error('PypoLiqQueue Exception: %s', traceback.format_exc())
+
+
+
diff --git a/python_apps/pypo/pypoliquidsoap.py b/python_apps/pypo/pypoliquidsoap.py
new file mode 100644
index 000000000..8a9554516
--- /dev/null
+++ b/python_apps/pypo/pypoliquidsoap.py
@@ -0,0 +1,230 @@
+from pypofetch import PypoFetch
+from telnetliquidsoap import TelnetLiquidsoap
+
+from datetime import datetime
+from datetime import timedelta
+
+import eventtypes
+import time
+
+class PypoLiquidsoap():
+ def __init__(self, logger, telnet_lock, host, port):
+ self.logger = logger
+ self.liq_queue_tracker = {
+ "s0": None,
+ "s1": None,
+ "s2": None,
+ "s3": None,
+ }
+
+ self.telnet_liquidsoap = TelnetLiquidsoap(telnet_lock, \
+ logger,\
+ host,\
+ port)
+
+
+ def play(self, media_item):
+ if media_item["type"] == eventtypes.FILE:
+ self.handle_file_type(media_item)
+ elif media_item["type"] == eventtypes.EVENT:
+ self.handle_event_type(media_item)
+ elif media_item["type"] == eventtypes.STREAM_BUFFER_START:
+ self.telnet_liquidsoap.start_web_stream_buffer(media_item)
+ elif media_item["type"] == eventtypes.STREAM_OUTPUT_START:
+ if media_item['row_id'] != self.telnet_liquidsoap.current_prebuffering_stream_id:
+ #this is called if the stream wasn't scheduled sufficiently ahead of time
+ #so that the prebuffering stage could take effect. Let's do the prebuffering now.
+ self.telnet_liquidsoap.start_web_stream_buffer(media_item)
+ self.telnet_liquidsoap.start_web_stream(media_item)
+ elif media_item['type'] == eventtypes.STREAM_BUFFER_END:
+ self.telnet_liquidsoap.stop_web_stream_buffer()
+ elif media_item['type'] == eventtypes.STREAM_OUTPUT_END:
+ self.telnet_liquidsoap.stop_web_stream_output()
+ else: raise UnknownMediaItemType(str(media_item))
+
+ def handle_file_type(self, media_item):
+ """
+ Wait maximum 5 seconds (50 iterations) for file to become ready,
+ otherwise give up on it.
+ """
+ iter_num = 0
+ while not media_item['file_ready'] and iter_num < 50:
+ time.sleep(0.1)
+ iter_num += 1
+
+ if media_item['file_ready']:
+ available_queue = self.find_available_queue()
+
+ try:
+ self.telnet_liquidsoap.queue_push(available_queue, media_item)
+ self.liq_queue_tracker[available_queue] = media_item
+ except Exception as e:
+ self.logger.error(e)
+ raise
+ else:
+ self.logger.warn("File %s did not become ready in less than 5 seconds. Skipping...", media_item['dst'])
+
+ def handle_event_type(self, media_item):
+ if media_item['event_type'] == "kick_out":
+ PypoFetch.disconnect_source(self.logger, self.telnet_lock, "live_dj")
+ elif media_item['event_type'] == "switch_off":
+ PypoFetch.switch_source(self.logger, self.telnet_lock, "live_dj", "off")
+
+
+ def is_media_item_finished(self, media_item):
+ if media_item is None:
+ return True
+ else:
+ return datetime.utcnow() > media_item['end']
+
+ def find_available_queue(self):
+ available_queue = None
+ for i in self.liq_queue_tracker:
+ mi = self.liq_queue_tracker[i]
+ if mi == None or self.is_media_item_finished(mi):
+ #queue "i" is available. Push to this queue
+ available_queue = i
+
+ if available_queue == None:
+ raise NoQueueAvailableException()
+
+ return available_queue
+
+ def get_queues():
+ return self.liq_queue_tracker
+
+
+ def verify_correct_present_media(self, scheduled_now):
+ #verify whether Liquidsoap is currently playing the correct files.
+ #if we find an item that Liquidsoap is not playing, then push it
+ #into one of Liquidsoap's queues. If Liquidsoap is already playing
+ #it do nothing. If Liquidsoap is playing a track that isn't in
+ #currently_playing then stop it.
+
+ #Check for Liquidsoap media we should source.skip
+ #get liquidsoap items for each queue. Since each queue can only have one
+ #item, we should have a max of 8 items.
+
+ #TODO: Verify start, end, replay_gain is the same
+
+ #2013-03-21-22-56-00_0: {
+ #id: 1,
+ #type: "stream_output_start",
+ #row_id: 41,
+ #uri: "http://stream2.radioblackout.org:80/blackout.ogg",
+ #start: "2013-03-21-22-56-00",
+ #end: "2013-03-21-23-26-00",
+ #show_name: "Untitled Show",
+ #independent_event: true
+ #},
+
+
+ scheduled_now_files = \
+ filter(lambda x: x["type"] == eventtypes.FILE, scheduled_now)
+
+ scheduled_now_webstream = \
+ filter(lambda x: x["type"] == eventtypes.STREAM_OUTPUT_START, \
+ scheduled_now)
+
+ schedule_ids = set(map(lambda x: x["row_id"], scheduled_now_files))
+
+ row_id_map = {}
+ liq_queue_ids = set()
+ for i in self.liq_queue_tracker:
+ mi = self.liq_queue_tracker[i]
+ if not self.is_media_item_finished(mi):
+ liq_queue_ids.add(mi["row_id"])
+ row_id_map[mi["row_id"]] = mi
+
+ to_be_removed = set()
+ to_be_added = set()
+
+ #Iterate over the new files, and compare them to currently scheduled
+ #tracks. If already in liquidsoap queue still need to make sure they don't
+ #have different attributes such replay_gain etc.
+ for i in scheduled_now_files:
+ if i["row_id"] in row_id_map:
+ mi = row_id_map[i["row_id"]]
+ correct = mi['start'] == i['start'] and \
+ mi['end'] == i['end'] and \
+ mi['replay_gain'] == i['replay_gain']
+
+ if not correct:
+ #need to re-add
+ self.logger.info("Track %s found to have new attr." % i)
+ to_be_removed.add(i["row_id"])
+ to_be_added.add(i["row_id"])
+
+
+ to_be_removed.update(liq_queue_ids - schedule_ids)
+ to_be_added.update(schedule_ids - liq_queue_ids)
+
+ if len(to_be_removed):
+ self.logger.info("Need to remove items from Liquidsoap: %s" % \
+ to_be_removed)
+
+ #remove files from Liquidsoap's queue
+ for i in self.liq_queue_tracker:
+ mi = self.liq_queue_tracker[i]
+ if mi is not None and mi["row_id"] in to_be_removed:
+ self.stop(i)
+
+ if len(to_be_added):
+ self.logger.info("Need to add items to Liquidsoap *now*: %s" % \
+ to_be_added)
+
+ for i in scheduled_now:
+ if i["row_id"] in to_be_added:
+ self.modify_cue_point(i)
+ self.play(i)
+
+ #handle webstreams
+ current_stream_id = self.telnet_liquidsoap.get_current_stream_id()
+ if len(scheduled_now_webstream):
+ if current_stream_id != scheduled_now_webstream[0]:
+ self.play(scheduled_now_webstream[0])
+ elif current_stream_id != "-1":
+ #something is playing and it shouldn't be.
+ self.telnet_liquidsoap.stop_web_stream_buffer()
+ self.telnet_liquidsoap.stop_web_stream_output()
+
+ def stop(self, queue):
+ self.telnet_liquidsoap.queue_remove(queue)
+ self.liq_queue_tracker[queue] = None
+
+ def is_file(self, media_item):
+ return media_item["type"] == eventtypes.FILE
+
+ def modify_cue_point(self, link):
+ if not self.is_file(link):
+ return
+
+ tnow = datetime.utcnow()
+
+ link_start = link['start']
+
+ diff_td = tnow - link_start
+ diff_sec = self.date_interval_to_seconds(diff_td)
+
+ if diff_sec > 0:
+ self.logger.debug("media item was supposed to start %s ago. Preparing to start..", diff_sec)
+ original_cue_in_td = timedelta(seconds=float(link['cue_in']))
+ link['cue_in'] = self.date_interval_to_seconds(original_cue_in_td) + diff_sec
+
+ def date_interval_to_seconds(self, interval):
+ """
+ Convert timedelta object into int representing the number of seconds. If
+ number of seconds is less than 0, then return 0.
+ """
+ seconds = (interval.microseconds + \
+ (interval.seconds + interval.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
+ if seconds < 0: seconds = 0
+
+ return seconds
+
+
+class UnknownMediaItemType(Exception):
+ pass
+
+class NoQueueAvailableException(Exception):
+ pass
diff --git a/python_apps/pypo/pypomessagehandler.py b/python_apps/pypo/pypomessagehandler.py
index 396f13fba..427772c19 100644
--- a/python_apps/pypo/pypomessagehandler.py
+++ b/python_apps/pypo/pypomessagehandler.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import logging
+import traceback
import sys
from configobj import ConfigObj
from threading import Thread
@@ -9,6 +10,7 @@ import time
from kombu.connection import BrokerConnection
from kombu.messaging import Exchange, Queue
from kombu.simple import SimpleQueue
+from amqplib.client_0_8.exceptions import AMQPConnectionException
import json
from std_err_override import LogWriter
@@ -111,11 +113,9 @@ class PypoMessageHandler(Thread):
self.handle_message(message.payload)
# ACK the message to take it off the queue
message.ack()
- except (IOError, AttributeError), e:
- import traceback
- top = traceback.format_exc()
+ except (IOError, AttributeError, AMQPConnectionException), e:
self.logger.error('Exception: %s', e)
- self.logger.error("traceback: %s", top)
+ self.logger.error("traceback: %s", traceback.format_exc())
while not self.init_rabbit_mq():
self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds")
time.sleep(5)
diff --git a/python_apps/pypo/pyponotify.py b/python_apps/pypo/pyponotify.py
index 9c2f1688c..797d1ce9b 100644
--- a/python_apps/pypo/pyponotify.py
+++ b/python_apps/pypo/pyponotify.py
@@ -43,7 +43,7 @@ parser.add_option("-t", "--time", help="Liquidsoap boot up time", action="store"
parser.add_option("-x", "--source-name", help="source connection name", metavar="source_name")
parser.add_option("-y", "--source-status", help="source connection status", metavar="source_status")
parser.add_option("-w", "--webstream", help="JSON metadata associated with webstream", metavar="json_data")
-parser.add_option("-n", "--liquidsoap-started", help="notify liquidsoap started", metavar="json_data", action="store_true", default=True)
+parser.add_option("-n", "--liquidsoap-started", help="notify liquidsoap started", metavar="json_data", action="store_true", default=False)
# parse options
diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py
index 6fedfab1b..4d6c21964 100644
--- a/python_apps/pypo/pypopush.py
+++ b/python_apps/pypo/pypopush.py
@@ -9,9 +9,14 @@ import logging.config
import telnetlib
import calendar
import math
-from pypofetch import PypoFetch
+import traceback
+import os
-from Queue import Empty
+from pypofetch import PypoFetch
+from pypoliqqueue import PypoLiqQueue
+from pypoliquidsoap import PypoLiquidsoap
+
+from Queue import Empty, Queue
from threading import Thread
@@ -21,7 +26,8 @@ from configobj import ConfigObj
# configure logging
-logging.config.fileConfig("logging.cfg")
+logging_cfg = os.path.join(os.path.dirname(__file__), "logging.cfg")
+logging.config.fileConfig(logging_cfg)
logger = logging.getLogger()
LogWriter.override_std_err(logger)
@@ -34,7 +40,6 @@ try:
LS_HOST = config['ls_host']
LS_PORT = config['ls_port']
PUSH_INTERVAL = 2
- MAX_LIQUIDSOAP_QUEUE_LENGTH = 2
except Exception, e:
logger.error('Error loading config file %s', e)
sys.exit()
@@ -56,88 +61,67 @@ class PypoPush(Thread):
self.pushed_objects = {}
self.logger = logging.getLogger('push')
self.current_prebuffering_stream_id = None
+ self.queue_id = 0
+
+ self.future_scheduled_queue = Queue()
+ self.pypo_liquidsoap = PypoLiquidsoap(self.logger, telnet_lock,\
+ LS_HOST, LS_PORT)
+
+ self.plq = PypoLiqQueue(self.future_scheduled_queue, \
+ self.pypo_liquidsoap, \
+ self.logger)
+ self.plq.daemon = True
+ self.plq.start()
+
def main(self):
loops = 0
heartbeat_period = math.floor(30 / PUSH_INTERVAL)
- next_media_item_chain = None
media_schedule = None
- time_until_next_play = None
- chains = None
while True:
try:
- if time_until_next_play is None:
- media_schedule = self.queue.get(block=True)
- else:
- media_schedule = self.queue.get(block=True, timeout=time_until_next_play)
-
- chains = self.get_all_chains(media_schedule)
-
- #We get to the following lines only if a schedule was received.
- liquidsoap_queue_approx = self.get_queue_items_from_liquidsoap()
- liquidsoap_stream_id = self.get_current_stream_id_from_liquidsoap()
-
- tnow = datetime.utcnow()
- current_event_chain, original_chain = self.get_current_chain(chains, tnow)
-
- if len(current_event_chain) > 0:
- try:
- chains.remove(original_chain)
- except ValueError, e:
- self.logger.error(str(e))
-
- #At this point we know that Liquidsoap is playing something, and that something
- #is scheduled. We need to verify whether the schedule we just received matches
- #what Liquidsoap is playing, and if not, correct it.
-
- self.handle_new_schedule(media_schedule, liquidsoap_queue_approx, liquidsoap_stream_id, current_event_chain)
-
-
- #At this point everything in the present has been taken care of and Liquidsoap
- #is playing whatever is scheduled.
- #Now we need to prepare ourselves for future scheduled events.
- #
- next_media_item_chain = self.get_next_schedule_chain(chains, tnow)
-
- self.logger.debug("Next schedule chain: %s", next_media_item_chain)
- if next_media_item_chain is not None:
- try:
- chains.remove(next_media_item_chain)
- except ValueError, e:
- self.logger.error(str(e))
-
- chain_start = datetime.strptime(next_media_item_chain[0]['start'], "%Y-%m-%d-%H-%M-%S")
- time_until_next_play = self.date_interval_to_seconds(chain_start - datetime.utcnow())
- self.logger.debug("Blocking %s seconds until show start", time_until_next_play)
- else:
- self.logger.debug("Blocking indefinitely since no show scheduled")
- time_until_next_play = None
- except Empty, e:
- #We only get here when a new chain of tracks are ready to be played.
- self.push_to_liquidsoap(next_media_item_chain)
-
- next_media_item_chain = self.get_next_schedule_chain(chains, datetime.utcnow())
- if next_media_item_chain is not None:
- try:
- chains.remove(next_media_item_chain)
- except ValueError, e:
- self.logger.error(str(e))
- chain_start = datetime.strptime(next_media_item_chain[0]['start'], "%Y-%m-%d-%H-%M-%S")
- time_until_next_play = self.date_interval_to_seconds(chain_start - datetime.utcnow())
- self.logger.debug("Blocking %s seconds until show start", time_until_next_play)
- else:
- self.logger.debug("Blocking indefinitely since no show scheduled next")
- time_until_next_play = None
+ media_schedule = self.queue.get(block=True)
except Exception, e:
self.logger.error(str(e))
+ raise
+ else:
+ self.logger.debug(media_schedule)
+ #separate media_schedule list into currently_playing and
+ #scheduled_for_future lists
+ currently_playing, scheduled_for_future = \
+ self.separate_present_future(media_schedule)
+
+ self.pypo_liquidsoap.verify_correct_present_media(currently_playing)
+ self.future_scheduled_queue.put(scheduled_for_future)
if loops % heartbeat_period == 0:
self.logger.info("heartbeat")
loops = 0
loops += 1
+
+ def separate_present_future(self, media_schedule):
+ tnow = datetime.utcnow()
+
+ present = []
+ future = {}
+
+ sorted_keys = sorted(media_schedule.keys())
+ for mkey in sorted_keys:
+ media_item = media_schedule[mkey]
+
+ diff_td = tnow - media_item['start']
+ diff_sec = self.date_interval_to_seconds(diff_td)
+
+ if diff_sec >= 0:
+ present.append(media_item)
+ else:
+ future[media_item['start']] = media_item
+
+ return present, future
+
def get_current_stream_id_from_liquidsoap(self):
response = "-1"
try:
@@ -157,267 +141,24 @@ class PypoPush(Thread):
return response
- def get_queue_items_from_liquidsoap(self):
- """
- This function connects to Liquidsoap to find what media items are in its queue.
- """
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
-
- msg = 'queue.queue\n'
- tn.write(msg)
- response = tn.read_until("\r\n").strip(" \r\n")
- tn.write('exit\n')
- tn.read_all()
- except Exception, e:
- self.logger.error("Error connecting to Liquidsoap: %s", e)
- response = []
- finally:
- self.telnet_lock.release()
-
- liquidsoap_queue_approx = []
-
- if len(response) > 0:
- items_in_queue = response.split(" ")
-
- self.logger.debug("items_in_queue: %s", items_in_queue)
-
- for item in items_in_queue:
- if item in self.pushed_objects:
- liquidsoap_queue_approx.append(self.pushed_objects[item])
- else:
- """
- We should only reach here if Pypo crashed and restarted (because self.pushed_objects was reset). In this case
- let's clear the entire Liquidsoap queue.
- """
- self.logger.error("ID exists in liquidsoap queue that does not exist in our pushed_objects queue: " + item)
- self.clear_liquidsoap_queue()
- liquidsoap_queue_approx = []
- break
-
- return liquidsoap_queue_approx
-
- def is_correct_current_item(self, media_item, liquidsoap_queue_approx, liquidsoap_stream_id):
- correct = False
- if media_item is None:
- correct = (len(liquidsoap_queue_approx) == 0 and liquidsoap_stream_id == "-1")
- else:
- if is_file(media_item):
- if len(liquidsoap_queue_approx) == 0:
- correct = False
- else:
- correct = liquidsoap_queue_approx[0]['start'] == media_item['start'] and \
- liquidsoap_queue_approx[0]['row_id'] == media_item['row_id'] and \
- liquidsoap_queue_approx[0]['end'] == media_item['end'] and \
- liquidsoap_queue_approx[0]['replay_gain'] == media_item['replay_gain']
- elif is_stream(media_item):
- correct = liquidsoap_stream_id == str(media_item['row_id'])
-
- self.logger.debug("Is current item correct?: %s", str(correct))
- return correct
-
-
- #clear all webstreams and files from Liquidsoap
- def clear_all_liquidsoap_items(self):
- self.remove_from_liquidsoap_queue(0, None)
- self.stop_web_stream_all()
-
- def handle_new_schedule(self, media_schedule, liquidsoap_queue_approx, liquidsoap_stream_id, current_event_chain):
- """
- This function's purpose is to gracefully handle situations where
- Liquidsoap already has a track in its queue, but the schedule
- has changed. If the schedule has changed, this function's job is to
- call other functions that will connect to Liquidsoap and alter its
- queue.
- """
- file_chain = filter(lambda item: (item["type"] == "file"), current_event_chain)
- stream_chain = filter(lambda item: (item["type"] == "stream_output_start"), current_event_chain)
-
- self.logger.debug(current_event_chain)
-
- #Take care of the case where the current playing may be incorrect
- if len(current_event_chain) > 0:
-
- current_item = current_event_chain[0]
- if not self.is_correct_current_item(current_item, liquidsoap_queue_approx, liquidsoap_stream_id):
- self.clear_all_liquidsoap_items()
- if is_stream(current_item):
- if current_item['row_id'] != self.current_prebuffering_stream_id:
- #this is called if the stream wasn't scheduled sufficiently ahead of time
- #so that the prebuffering stage could take effect. Let's do the prebuffering now.
- self.start_web_stream_buffer(current_item)
- self.start_web_stream(current_item)
- if is_file(current_item):
- self.modify_cue_point(file_chain[0])
- self.push_to_liquidsoap(file_chain)
- #we've changed the queue, so let's refetch it
- liquidsoap_queue_approx = self.get_queue_items_from_liquidsoap()
-
- elif not self.is_correct_current_item(None, liquidsoap_queue_approx, liquidsoap_stream_id):
- #Liquidsoap is playing something even though it shouldn't be
- self.clear_all_liquidsoap_items()
-
-
- #If the current item scheduled is a file, then files come in chains, and
- #therefore we need to make sure the entire chain is correct.
- if len(current_event_chain) > 0 and is_file(current_event_chain[0]):
- problem_at_iteration = self.find_removed_items(media_schedule, liquidsoap_queue_approx)
-
- if problem_at_iteration is not None:
- #Items that are in Liquidsoap's queue aren't scheduled anymore. We need to connect
- #and remove these items.
- self.logger.debug("Change in link %s of current chain", problem_at_iteration)
- self.remove_from_liquidsoap_queue(problem_at_iteration, liquidsoap_queue_approx[problem_at_iteration:])
-
- if problem_at_iteration is None and len(file_chain) > len(liquidsoap_queue_approx):
- self.logger.debug("New schedule has longer current chain.")
- problem_at_iteration = len(liquidsoap_queue_approx)
-
- if problem_at_iteration is not None:
- self.logger.debug("Change in chain at link %s", problem_at_iteration)
-
- chain_to_push = file_chain[problem_at_iteration:]
- if len(chain_to_push) > 0:
- self.modify_cue_point(chain_to_push[0])
- self.push_to_liquidsoap(chain_to_push)
-
-
- """
- Compare whats in the liquidsoap_queue to the new schedule we just
- received in media_schedule. This function only iterates over liquidsoap_queue_approx
- and finds if every item in that list is still scheduled in "media_schedule". It doesn't
- take care of the case where media_schedule has more items than liquidsoap_queue_approx
- """
- def find_removed_items(self, media_schedule, liquidsoap_queue_approx):
- #iterate through the items we got from the liquidsoap queue and
- #see if they are the same as the newly received schedule
- iteration = 0
- problem_at_iteration = None
- for queue_item in liquidsoap_queue_approx:
- if queue_item['start'] in media_schedule.keys():
- media_item = media_schedule[queue_item['start']]
- if queue_item['row_id'] == media_item['row_id']:
- if queue_item['end'] == media_item['end']:
- #Everything OK for this iteration.
- pass
- else:
- problem_at_iteration = iteration
- break
- else:
- #A different item has been scheduled at the same time! Need to remove
- #all tracks from the Liquidsoap queue starting at this point, and re-add
- #them.
- problem_at_iteration = iteration
- break
- else:
- #There are no more items scheduled for this time! The user has shortened
- #the playlist, so we simply need to remove tracks from the queue.
- problem_at_iteration = iteration
- break
- iteration += 1
- return problem_at_iteration
-
-
-
- def get_all_chains(self, media_schedule):
- chains = []
-
- current_chain = []
-
- sorted_keys = sorted(media_schedule.keys())
-
- for mkey in sorted_keys:
- media_item = media_schedule[mkey]
- if media_item['independent_event']:
- if len(current_chain) > 0:
- chains.append(current_chain)
-
- chains.append([media_item])
- current_chain = []
- elif len(current_chain) == 0:
- current_chain.append(media_item)
- elif media_item['start'] == current_chain[-1]['end']:
- current_chain.append(media_item)
- else:
- #current item is not a continuation of the chain.
- #Start a new one instead
- chains.append(current_chain)
- current_chain = [media_item]
-
- if len(current_chain) > 0:
- chains.append(current_chain)
-
- return chains
-
- def modify_cue_point(self, link):
- tnow = datetime.utcnow()
-
- link_start = datetime.strptime(link['start'], "%Y-%m-%d-%H-%M-%S")
-
- diff_td = tnow - link_start
- diff_sec = self.date_interval_to_seconds(diff_td)
-
- if diff_sec > 0:
- self.logger.debug("media item was supposed to start %s ago. Preparing to start..", diff_sec)
- original_cue_in_td = timedelta(seconds=float(link['cue_in']))
- link['cue_in'] = self.date_interval_to_seconds(original_cue_in_td) + diff_sec
-
- """
- Returns two chains, original chain and current_chain. current_chain is a subset of
- original_chain but can also be equal to original chain.
-
- We return original chain because the user of this function may want to clean
- up the input 'chains' list
-
- chain, original = get_current_chain(chains)
-
- and
- chains.remove(chain) can throw a ValueError exception
-
- but
- chains.remove(original) won't
- """
- def get_current_chain(self, chains, tnow):
- current_chain = []
- original_chain = None
-
- for chain in chains:
- iteration = 0
- for link in chain:
- link_start = datetime.strptime(link['start'], "%Y-%m-%d-%H-%M-%S")
- link_end = datetime.strptime(link['end'], "%Y-%m-%d-%H-%M-%S")
-
- self.logger.debug("tnow %s, chain_start %s", tnow, link_start)
- if link_start <= tnow and tnow < link_end:
- current_chain = chain[iteration:]
- original_chain = chain
- break
- iteration += 1
-
- return current_chain, original_chain
-
- """
- The purpose of this function is to take a look at the last received schedule from
- pypo-fetch and return the next chain of media_items. A chain is defined as a sequence
- of media_items where the end time of media_item 'n' is the start time of media_item
- 'n+1'
- """
- def get_next_schedule_chain(self, chains, tnow):
- #all media_items are now divided into chains. Let's find the one that
- #starts closest in the future.
- closest_start = None
- closest_chain = None
- for chain in chains:
- chain_start = datetime.strptime(chain[0]['start'], "%Y-%m-%d-%H-%M-%S")
- chain_end = datetime.strptime(chain[-1]['end'], "%Y-%m-%d-%H-%M-%S")
- self.logger.debug("tnow %s, chain_start %s", tnow, chain_start)
- if (closest_start == None or chain_start < closest_start) and (chain_start > tnow or (chain_start < tnow and chain_end > tnow)):
- closest_start = chain_start
- closest_chain = chain
-
- return closest_chain
+ #def is_correct_current_item(self, media_item, liquidsoap_queue_approx, liquidsoap_stream_id):
+ #correct = False
+ #if media_item is None:
+ #correct = (len(liquidsoap_queue_approx) == 0 and liquidsoap_stream_id == "-1")
+ #else:
+ #if is_file(media_item):
+ #if len(liquidsoap_queue_approx) == 0:
+ #correct = False
+ #else:
+ #correct = liquidsoap_queue_approx[0]['start'] == media_item['start'] and \
+ #liquidsoap_queue_approx[0]['row_id'] == media_item['row_id'] and \
+ #liquidsoap_queue_approx[0]['end'] == media_item['end'] and \
+ #liquidsoap_queue_approx[0]['replay_gain'] == media_item['replay_gain']
+ #elif is_stream(media_item):
+ #correct = liquidsoap_stream_id == str(media_item['row_id'])
+ #self.logger.debug("Is current item correct?: %s", str(correct))
+ #return correct
def date_interval_to_seconds(self, interval):
"""
@@ -426,96 +167,9 @@ class PypoPush(Thread):
"""
seconds = (interval.microseconds + \
(interval.seconds + interval.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
- if seconds < 0: seconds = 0
return seconds
- def push_to_liquidsoap(self, event_chain):
-
- try:
- for media_item in event_chain:
- if media_item['type'] == "file":
-
- """
- Wait maximum 5 seconds (50 iterations) for file to become ready, otherwise
- give up on it.
- """
- iter_num = 0
- while not media_item['file_ready'] and iter_num < 50:
- time.sleep(0.1)
- iter_num += 1
-
- if media_item['file_ready']:
- self.telnet_to_liquidsoap(media_item)
- else:
- self.logger.warn("File %s did not become ready in less than 5 seconds. Skipping...", media_item['dst'])
- elif media_item['type'] == "event":
- if media_item['event_type'] == "kick_out":
- PypoFetch.disconnect_source(self.logger, self.telnet_lock, "live_dj")
- elif media_item['event_type'] == "switch_off":
- PypoFetch.switch_source(self.logger, self.telnet_lock, "live_dj", "off")
- elif media_item['type'] == 'stream_buffer_start':
- self.start_web_stream_buffer(media_item)
- elif media_item['type'] == "stream_output_start":
- if media_item['row_id'] != self.current_prebuffering_stream_id:
- #this is called if the stream wasn't scheduled sufficiently ahead of time
- #so that the prebuffering stage could take effect. Let's do the prebuffering now.
- self.start_web_stream_buffer(media_item)
- self.start_web_stream(media_item)
- elif media_item['type'] == "stream_buffer_end":
- self.stop_web_stream_buffer(media_item)
- elif media_item['type'] == "stream_output_end":
- self.stop_web_stream_output(media_item)
- except Exception, e:
- self.logger.error('Pypo Push Exception: %s', e)
-
-
- def start_web_stream_buffer(self, media_item):
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
-
- msg = 'dynamic_source.id %s\n' % media_item['row_id']
- self.logger.debug(msg)
- tn.write(msg)
-
- #msg = 'dynamic_source.read_start %s\n' % media_item['uri'].encode('latin-1')
- msg = 'http.restart %s\n' % media_item['uri'].encode('latin-1')
- self.logger.debug(msg)
- tn.write(msg)
-
- tn.write("exit\n")
- self.logger.debug(tn.read_all())
-
- self.current_prebuffering_stream_id = media_item['row_id']
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
-
- def start_web_stream(self, media_item):
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
-
- #TODO: DO we need this?
- msg = 'streams.scheduled_play_start\n'
- tn.write(msg)
-
- msg = 'dynamic_source.output_start\n'
- self.logger.debug(msg)
- tn.write(msg)
-
- tn.write("exit\n")
- self.logger.debug(tn.read_all())
-
- self.current_prebuffering_stream_id = None
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
def stop_web_stream_all(self):
try:
self.telnet_lock.acquire()
@@ -542,173 +196,9 @@ class PypoPush(Thread):
finally:
self.telnet_lock.release()
- def stop_web_stream_buffer(self, media_item):
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
- #dynamic_source.stop http://87.230.101.24:80/top100station.mp3
-
- #msg = 'dynamic_source.read_stop %s\n' % media_item['row_id']
- msg = 'http.stop\n'
- self.logger.debug(msg)
- tn.write(msg)
-
- msg = 'dynamic_source.id -1\n'
- self.logger.debug(msg)
- tn.write(msg)
-
- tn.write("exit\n")
- self.logger.debug(tn.read_all())
-
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
- def stop_web_stream_output(self, media_item):
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
- #dynamic_source.stop http://87.230.101.24:80/top100station.mp3
-
- msg = 'dynamic_source.output_stop\n'
- self.logger.debug(msg)
- tn.write(msg)
-
- tn.write("exit\n")
- self.logger.debug(tn.read_all())
-
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
- def clear_liquidsoap_queue(self):
- self.logger.debug("Clearing Liquidsoap queue")
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
- msg = "source.skip\n"
- tn.write(msg)
- tn.write("exit\n")
- tn.read_all()
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
- def remove_from_liquidsoap_queue(self, problem_at_iteration, liquidsoap_queue_approx):
-
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
-
- if problem_at_iteration == 0:
- msg = "source.skip\n"
- self.logger.debug(msg)
- tn.write(msg)
- else:
- # Remove things in reverse order.
- queue_copy = liquidsoap_queue_approx[::-1]
-
- for queue_item in queue_copy:
- msg = "queue.remove %s\n" % queue_item['queue_id']
- self.logger.debug(msg)
- tn.write(msg)
- response = tn.read_until("\r\n").strip("\r\n")
-
- if "No such request in my queue" in response:
- """
- Cannot remove because Liquidsoap started playing the item. Need
- to use source.skip instead
- """
- msg = "source.skip\n"
- self.logger.debug(msg)
- tn.write(msg)
-
- msg = "queue.queue\n"
- self.logger.debug(msg)
- tn.write(msg)
-
- tn.write("exit\n")
- self.logger.debug(tn.read_all())
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
- def sleep_until_start(self, media_item):
- """
- The purpose of this function is to look at the difference between
- "now" and when the media_item starts, and sleep for that period of time.
- After waking from sleep, this function returns.
- """
-
- mi_start = media_item['start'][0:19]
-
- #strptime returns struct_time in local time
- epoch_start = calendar.timegm(time.strptime(mi_start, '%Y-%m-%d-%H-%M-%S'))
-
- #Return the time as a floating point number expressed in seconds since the epoch, in UTC.
- epoch_now = time.time()
-
- self.logger.debug("Epoch start: %s" % epoch_start)
- self.logger.debug("Epoch now: %s" % epoch_now)
-
- sleep_time = epoch_start - epoch_now
-
- if sleep_time < 0:
- sleep_time = 0
-
- self.logger.debug('sleeping for %s s' % (sleep_time))
- time.sleep(sleep_time)
-
- def telnet_to_liquidsoap(self, media_item):
- """
- telnets to liquidsoap and pushes the media_item to its queue. Push the
- show name of every media_item as well, just to keep Liquidsoap up-to-date
- about which show is playing.
- """
- try:
- self.telnet_lock.acquire()
- tn = telnetlib.Telnet(LS_HOST, LS_PORT)
-
- #tn.write(("vars.pypo_data %s\n"%liquidsoap_data["schedule_id"]).encode('utf-8'))
-
- annotation = self.create_liquidsoap_annotation(media_item)
- msg = 'queue.push %s\n' % annotation.encode('utf-8')
- self.logger.debug(msg)
- tn.write(msg)
- queue_id = tn.read_until("\r\n").strip("\r\n")
-
- #remember the media_item's queue id which we may use
- #later if we need to remove it from the queue.
- media_item['queue_id'] = queue_id
-
- #add media_item to the end of our queue
- self.pushed_objects[queue_id] = media_item
-
- show_name = media_item['show_name']
- msg = 'vars.show_name %s\n' % show_name.encode('utf-8')
- tn.write(msg)
- self.logger.debug(msg)
-
- tn.write("exit\n")
- self.logger.debug(tn.read_all())
- except Exception, e:
- self.logger.error(str(e))
- finally:
- self.telnet_lock.release()
-
- def create_liquidsoap_annotation(self, media):
- # We need liq_start_next value in the annotate. That is the value that controls overlap duration of crossfade.
- return 'annotate:media_id="%s",liq_start_next="0",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s",replay_gain="%s dB":%s' \
- % (media['id'], float(media['fade_in']) / 1000, float(media['fade_out']) / 1000, float(media['cue_in']), float(media['cue_out']), media['row_id'], media['replay_gain'], media['dst'])
-
def run(self):
try: self.main()
except Exception, e:
- import traceback
top = traceback.format_exc()
self.logger.error('Pypo Push Exception: %s', top)
diff --git a/python_apps/pypo/recorder.py b/python_apps/pypo/recorder.py
index fbabc1b57..b3818f32d 100644
--- a/python_apps/pypo/recorder.py
+++ b/python_apps/pypo/recorder.py
@@ -189,9 +189,17 @@ class Recorder(Thread):
self.server_timezone = ''
self.queue = q
self.loops = 0
- self.api_client.register_component("show-recorder")
self.logger.info("RecorderFetch: init complete")
+ success = False
+ while not success:
+ try:
+ self.api_client.register_component('show-recorder')
+ success = True
+ except Exception, e:
+ self.logger.error(str(e))
+ time.sleep(10)
+
def handle_message(self):
if not self.queue.empty():
message = self.queue.get()
@@ -295,8 +303,6 @@ class Recorder(Thread):
heartbeat_period = math.floor(30 / PUSH_INTERVAL)
while True:
- if self.loops % heartbeat_period == 0:
- self.logger.info("heartbeat")
if self.loops * PUSH_INTERVAL > 3600:
self.loops = 0
"""
diff --git a/python_apps/pypo/telnetliquidsoap.py b/python_apps/pypo/telnetliquidsoap.py
new file mode 100644
index 000000000..223bc475e
--- /dev/null
+++ b/python_apps/pypo/telnetliquidsoap.py
@@ -0,0 +1,208 @@
+import telnetlib
+
+def create_liquidsoap_annotation(media):
+ # We need liq_start_next value in the annotate. That is the value that controls overlap duration of crossfade.
+ return 'annotate:media_id="%s",liq_start_next="0",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s",replay_gain="%s dB":%s' \
+ % (media['id'], float(media['fade_in']) / 1000, float(media['fade_out']) / 1000, float(media['cue_in']), float(media['cue_out']), media['row_id'], media['replay_gain'], media['dst'])
+
+class TelnetLiquidsoap:
+
+ def __init__(self, telnet_lock, logger, ls_host, ls_port):
+ self.telnet_lock = telnet_lock
+ self.ls_host = ls_host
+ self.ls_port = ls_port
+ self.logger = logger
+ self.current_prebuffering_stream_id = None
+
+ def __connect(self):
+ return telnetlib.Telnet(self.ls_host, self.ls_port)
+
+ def __is_empty(self, tn, queue_id):
+ return True
+
+ def queue_remove(self, queue_id):
+ try:
+ self.telnet_lock.acquire()
+ tn = self.__connect()
+
+ msg = 'queues.%s_skip\n' % queue_id
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ tn.write("exit\n")
+ self.logger.debug(tn.read_all())
+ except Exception:
+ raise
+ finally:
+ self.telnet_lock.release()
+
+
+ def queue_push(self, queue_id, media_item):
+ try:
+ self.telnet_lock.acquire()
+ tn = self.__connect()
+
+ if not self.__is_empty(tn, queue_id):
+ raise QueueNotEmptyException()
+
+ annotation = create_liquidsoap_annotation(media_item)
+ msg = '%s.push %s\n' % (queue_id, annotation.encode('utf-8'))
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ show_name = media_item['show_name']
+ msg = 'vars.show_name %s\n' % show_name.encode('utf-8')
+ tn.write(msg)
+ self.logger.debug(msg)
+
+ tn.write("exit\n")
+ self.logger.debug(tn.read_all())
+ except Exception:
+ raise
+ finally:
+ self.telnet_lock.release()
+
+
+ def stop_web_stream_buffer(self):
+ try:
+ self.telnet_lock.acquire()
+ tn = telnetlib.Telnet(self.ls_host, self.ls_port)
+ #dynamic_source.stop http://87.230.101.24:80/top100station.mp3
+
+ msg = 'http.stop\n'
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ msg = 'dynamic_source.id -1\n'
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ tn.write("exit\n")
+ self.logger.debug(tn.read_all())
+
+ except Exception, e:
+ self.logger.error(str(e))
+ finally:
+ self.telnet_lock.release()
+
+ def stop_web_stream_output(self):
+ try:
+ self.telnet_lock.acquire()
+ tn = telnetlib.Telnet(self.ls_host, self.ls_port)
+ #dynamic_source.stop http://87.230.101.24:80/top100station.mp3
+
+ msg = 'dynamic_source.output_stop\n'
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ tn.write("exit\n")
+ self.logger.debug(tn.read_all())
+
+ except Exception, e:
+ self.logger.error(str(e))
+ finally:
+ self.telnet_lock.release()
+
+ def start_web_stream(self, media_item):
+ try:
+ self.telnet_lock.acquire()
+ tn = telnetlib.Telnet(self.ls_host, self.ls_port)
+
+ #TODO: DO we need this?
+ msg = 'streams.scheduled_play_start\n'
+ tn.write(msg)
+
+ msg = 'dynamic_source.output_start\n'
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ tn.write("exit\n")
+ self.logger.debug(tn.read_all())
+
+ self.current_prebuffering_stream_id = None
+ except Exception, e:
+ self.logger.error(str(e))
+ finally:
+ self.telnet_lock.release()
+
+ def start_web_stream_buffer(self, media_item):
+ try:
+ self.telnet_lock.acquire()
+ tn = telnetlib.Telnet(self.ls_host, self.ls_port)
+
+ msg = 'dynamic_source.id %s\n' % media_item['row_id']
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ msg = 'http.restart %s\n' % media_item['uri'].encode('latin-1')
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ tn.write("exit\n")
+ self.logger.debug(tn.read_all())
+
+ self.current_prebuffering_stream_id = media_item['row_id']
+ except Exception, e:
+ self.logger.error(str(e))
+ finally:
+ self.telnet_lock.release()
+
+ def get_current_stream_id(self):
+ try:
+ self.telnet_lock.acquire()
+ tn = telnetlib.Telnet(self.ls_host, self.ls_port)
+
+ msg = 'dynamic_source.get_id\n'
+ self.logger.debug(msg)
+ tn.write(msg)
+
+ tn.write("exit\n")
+ stream_id = tn.read_all().splitlines()[0]
+ self.logger.debug("stream_id: %s" % stream_id)
+
+ return stream_id
+ except Exception, e:
+ self.logger.error(str(e))
+ finally:
+ self.telnet_lock.release()
+
+class DummyTelnetLiquidsoap:
+
+ def __init__(self, telnet_lock, logger):
+ self.telnet_lock = telnet_lock
+ self.liquidsoap_mock_queues = {}
+ self.logger = logger
+
+ for i in range(4):
+ self.liquidsoap_mock_queues["s"+str(i)] = []
+
+ def queue_push(self, queue_id, media_item):
+ try:
+ self.telnet_lock.acquire()
+
+ self.logger.info("Pushing %s to queue %s" % (media_item, queue_id))
+ from datetime import datetime
+ print "Time now: %s" % datetime.utcnow()
+
+ annotation = create_liquidsoap_annotation(media_item)
+ self.liquidsoap_mock_queues[queue_id].append(annotation)
+ except Exception:
+ raise
+ finally:
+ self.telnet_lock.release()
+
+ def queue_remove(self, queue_id):
+ try:
+ self.telnet_lock.acquire()
+
+ self.logger.info("Purging queue %s" % queue_id)
+ from datetime import datetime
+ print "Time now: %s" % datetime.utcnow()
+
+ except Exception:
+ raise
+ finally:
+ self.telnet_lock.release()
+
+class QueueNotEmptyException(Exception):
+ pass
diff --git a/python_apps/pypo/testpypoliqqueue.py b/python_apps/pypo/testpypoliqqueue.py
new file mode 100644
index 000000000..f1847b34f
--- /dev/null
+++ b/python_apps/pypo/testpypoliqqueue.py
@@ -0,0 +1,98 @@
+from pypoliqqueue import PypoLiqQueue
+from telnetliquidsoap import DummyTelnetLiquidsoap, TelnetLiquidsoap
+
+
+from Queue import Queue
+from threading import Lock
+
+import sys
+import signal
+import logging
+from datetime import datetime
+from datetime import timedelta
+
+def keyboardInterruptHandler(signum, frame):
+ logger = logging.getLogger()
+ logger.info('\nKeyboard Interrupt\n')
+ sys.exit(0)
+signal.signal(signal.SIGINT, keyboardInterruptHandler)
+
+# configure logging
+format = '%(levelname)s - %(pathname)s - %(lineno)s - %(asctime)s - %(message)s'
+logging.basicConfig(level=logging.DEBUG, format=format)
+logging.captureWarnings(True)
+
+telnet_lock = Lock()
+pypoPush_q = Queue()
+
+
+pypoLiq_q = Queue()
+liq_queue_tracker = {
+ "s0": None,
+ "s1": None,
+ "s2": None,
+ "s3": None,
+ }
+
+#dummy_telnet_liquidsoap = DummyTelnetLiquidsoap(telnet_lock, logging)
+dummy_telnet_liquidsoap = TelnetLiquidsoap(telnet_lock, logging, \
+ "localhost", \
+ 1234)
+
+plq = PypoLiqQueue(pypoLiq_q, telnet_lock, logging, liq_queue_tracker, \
+ dummy_telnet_liquidsoap)
+plq.daemon = True
+plq.start()
+
+
+print "Time now: %s" % datetime.utcnow()
+
+media_schedule = {}
+
+start_dt = datetime.utcnow() + timedelta(seconds=1)
+end_dt = datetime.utcnow() + timedelta(seconds=6)
+
+media_schedule[start_dt] = {"id": 5, \
+ "type":"file", \
+ "row_id":9, \
+ "uri":"", \
+ "dst":"/home/martin/Music/ipod/Hot Chocolate - You Sexy Thing.mp3", \
+ "fade_in":0, \
+ "fade_out":0, \
+ "cue_in":0, \
+ "cue_out":300, \
+ "start": start_dt, \
+ "end": end_dt, \
+ "show_name":"Untitled", \
+ "replay_gain": 0, \
+ "independent_event": True \
+ }
+
+
+
+start_dt = datetime.utcnow() + timedelta(seconds=2)
+end_dt = datetime.utcnow() + timedelta(seconds=6)
+
+media_schedule[start_dt] = {"id": 5, \
+ "type":"file", \
+ "row_id":9, \
+ "uri":"", \
+ "dst":"/home/martin/Music/ipod/Good Charlotte - bloody valentine.mp3", \
+ "fade_in":0, \
+ "fade_out":0, \
+ "cue_in":0, \
+ "cue_out":300, \
+ "start": start_dt, \
+ "end": end_dt, \
+ "show_name":"Untitled", \
+ "replay_gain": 0, \
+ "independent_event": True \
+ }
+pypoLiq_q.put(media_schedule)
+
+plq.join()
+
+
+
+
+
diff --git a/python_apps/pypo/tests/run_tests.sh b/python_apps/pypo/tests/run_tests.sh
new file mode 100755
index 000000000..830a9bb85
--- /dev/null
+++ b/python_apps/pypo/tests/run_tests.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+which py.test
+pytest_exist=$?
+
+if [ "$pytest_exist" != "0" ]; then
+ echo "Need to have py.test installed. Exiting..."
+ exit 1
+fi
+
+SCRIPT=`readlink -f $0`
+# Absolute directory this script is in
+SCRIPTPATH=`dirname $SCRIPT`
+
+export PYTHONPATH=$PYTHONPATH:$SCRIPTPATH/..:$SCRIPTPATH/../..
+
+py.test
+
diff --git a/python_apps/pypo/tests/test_modify_cue_in.py b/python_apps/pypo/tests/test_modify_cue_in.py
new file mode 100644
index 000000000..da17fd53f
--- /dev/null
+++ b/python_apps/pypo/tests/test_modify_cue_in.py
@@ -0,0 +1,26 @@
+from pypopush import PypoPush
+from threading import Lock
+from Queue import Queue
+
+import datetime
+
+pypoPush_q = Queue()
+telnet_lock = Lock()
+
+pp = PypoPush(pypoPush_q, telnet_lock)
+
+def test_modify_cue_in():
+ link = pp.modify_first_link_cue_point([])
+ assert len(link) == 0
+
+ min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes = 1)
+ link = [{"start":min_ago.strftime("%Y-%m-%d-%H-%M-%S"),
+ "cue_in":"0", "cue_out":"30"}]
+ link = pp.modify_first_link_cue_point(link)
+ assert len(link) == 0
+
+ link = [{"start":min_ago.strftime("%Y-%m-%d-%H-%M-%S"),
+ "cue_in":"0", "cue_out":"70"}]
+ link = pp.modify_first_link_cue_point(link)
+ assert len(link) == 1
+
diff --git a/python_apps/python-virtualenv/airtime_virtual_env.pybundle b/python_apps/python-virtualenv/airtime_virtual_env.pybundle
index 694793148..109804252 100644
Binary files a/python_apps/python-virtualenv/airtime_virtual_env.pybundle and b/python_apps/python-virtualenv/airtime_virtual_env.pybundle differ
diff --git a/python_apps/python-virtualenv/requirements b/python_apps/python-virtualenv/requirements
index 72748874a..77e561417 100644
--- a/python_apps/python-virtualenv/requirements
+++ b/python_apps/python-virtualenv/requirements
@@ -8,5 +8,5 @@ poster==0.8.1
pytz==2011k
wsgiref==0.1.2
configobj==4.7.2
-mutagen==1.20
+mutagen==1.21
docopt==0.4.2
diff --git a/python_apps/python-virtualenv/virtualenv-install.sh b/python_apps/python-virtualenv/virtualenv-install.sh
index 0d9a2e2b8..30991faab 100755
--- a/python_apps/python-virtualenv/virtualenv-install.sh
+++ b/python_apps/python-virtualenv/virtualenv-install.sh
@@ -50,18 +50,3 @@ fi
echo -e "\n*** Installing Python Libraries ***"
/usr/lib/airtime/airtime_virtualenv/bin/pip install ${SCRIPTPATH}/airtime_virtual_env.pybundle || exit 1
-
-PYTHON_VERSION=$(python -c "import sys; print 'python%s.%s' % (sys.version_info[0], sys.version_info[1])")
-
-echo -e "\n*** Patching Python Libraries ***"
-echo " * Patching virtualenv libraries in /usr/lib/airtime/airtime_virtualenv/lib/$PYTHON_VERSION"
-PATCHES=${SCRIPTPATH}/patches/*
-for file in $(find $PATCHES -print); do
-if [ -d $file ]; then
- DIRNAME=$(basename $file)
- echo -e "\n ---Applying Patches for $DIRNAME---"
-else
- patch -N -p7 -i $file -d /usr/lib/airtime/airtime_virtualenv/lib/$PYTHON_VERSION
-fi
-done
-exit 0
diff --git a/utils/airtime-backup.py b/utils/airtime-backup.py
new file mode 100644
index 000000000..8838d3c88
--- /dev/null
+++ b/utils/airtime-backup.py
@@ -0,0 +1,52 @@
+import os
+import sys
+import shutil
+
+#check if root
+if os.geteuid() != 0:
+ print 'Must be a root user.'
+ sys.exit(1)
+
+#ask if we should backup config files
+backup_config = True
+
+#ask if we should backup database
+backup_database = True
+
+#ask if we should backup stor directory
+backup_stor = True
+
+#ask if we should backup all watched directories
+backup_watched = True
+
+#create airtime-backup directory
+os.mkdir("airtime_backup")
+
+if backup_config:
+ backup_config_dir = "airtime_backup/config"
+ os.mkdir(backup_config_dir)
+ #TODO check if directory exists
+ config_dir = "/etc/airtime"
+ files = os.listdir()
+ for f in files:
+ shutil.copy(os.path.join(config_dir, f), \
+ os.path.join(backup_config_dir, f)
+
+if backup_database:
+ os.mkdir("airtime_backup/database")
+ #TODO: get database name
+ #TODO use abs path
+ "pg_dump airtime > database.dump.sql"
+
+#TODO this might not be necessary
+os.mkdir("airtime_backup/files")
+
+if backup_stor:
+ #TODO use abs path
+ backup_stor_dir = "airtime_backup/files/stor"
+ os.mkdir(backup_stor_dir)
+ shutil.copytree("/srv/airtime/stor", backup_stor_dir)
+
+if backup_watched:
+ pass
+
diff --git a/utils/airtime-check-system.php b/utils/airtime-check-system.php
index a780b1243..1ce017c35 100644
--- a/utils/airtime-check-system.php
+++ b/utils/airtime-check-system.php
@@ -99,10 +99,13 @@ class AirtimeCheck {
{
$headerInfo = get_headers("http://$p_baseUrl:$p_basePort",1);
- if (!isset($headerInfo['Server'][0]))
+ if (!isset($headerInfo['Server'][0])) {
return self::UNKNOWN;
- else
+ } else if (is_array($headerInfo['Server'])) {
return $headerInfo['Server'][0];
+ } else {
+ return $headerInfo['Server'];
+ }
}
public static function GetStatus($p_baseUrl, $p_basePort, $p_baseDir, $p_apiKey){
@@ -205,17 +208,6 @@ class AirtimeCheck {
$log = "/var/log/airtime/media-monitor/media-monitor.log";
self::show_log_file($log);
}
- if (isset($services->rabbitmq)) {
- self::output_status("RABBITMQ_PROCESS_ID", $data->services->rabbitmq->process_id);
- self::output_status("RABBITMQ_RUNNING_SECONDS", $data->services->rabbitmq->uptime_seconds);
- self::output_status("RABBITMQ_MEM_PERC", $data->services->rabbitmq->memory_perc);
- self::output_status("RABBITMQ_CPU_PERC", $data->services->rabbitmq->cpu_perc);
- } else {
- self::output_status("RABBITMQ_PROCESS_ID", "FAILED");
- self::output_status("RABBITMQ_RUNNING_SECONDS", "0");
- self::output_status("RABBITMQ_MEM_PERC", "0%");
- self::output_status("RABBITMQ_CPU_PERC", "0%");
- }
}
if (self::$AIRTIME_STATUS_OK){
diff --git a/utils/airtime-import/airtime-import.py b/utils/airtime-import/airtime-import.py
index 1e55c732c..97d4c4e35 100644
--- a/utils/airtime-import/airtime-import.py
+++ b/utils/airtime-import/airtime-import.py
@@ -75,15 +75,16 @@ def format_dir_string(path):
return path
def helper_get_stor_dir():
- res = api_client.list_all_watched_dirs()
- if(res is None):
+ try:
+ res = api_client.list_all_watched_dirs()
+ except Exception, e:
return res
+
+ if(res['dirs']['1'][-1] != '/'):
+ out = res['dirs']['1']+'/'
+ return out
else:
- if(res['dirs']['1'][-1] != '/'):
- out = res['dirs']['1']+'/'
- return out
- else:
- return res['dirs']['1']
+ return res['dirs']['1']
def checkOtherOption(args):
for i in args:
@@ -162,14 +163,17 @@ def WatchAddAction(option, opt, value, parser):
path = apc.encode_to(path, 'utf-8')
if(os.path.isdir(path)):
#os.chmod(path, 0765)
- res = api_client.add_watched_dir(path)
- if(res is None):
+ try:
+ res = api_client.add_watched_dir(path)
+ except Exception, e:
exit("Unable to connect to the server.")
# sucess
if(res['msg']['code'] == 0):
print "%s added to watched folder list successfully" % path
else:
print "Adding a watched folder failed: %s" % res['msg']['error']
+ print "This error most likely caused by wrong permissions"
+ print "Try fixing this error by chmodding the parent directory(ies)"
else:
print "Given path is not a directory: %s" % path
@@ -177,8 +181,9 @@ def WatchListAction(option, opt, value, parser):
errorIfMultipleOption(parser.rargs)
if(len(parser.rargs) > 0):
raise OptionValueError("This option doesn't take any arguments.")
- res = api_client.list_all_watched_dirs()
- if(res is None):
+ try:
+ res = api_client.list_all_watched_dirs()
+ except Exception, e:
exit("Unable to connect to the Airtime server.")
dirs = res["dirs"].items()
# there will be always 1 which is storage folder
@@ -202,8 +207,9 @@ def WatchRemoveAction(option, opt, value, parser):
path = currentDir+path
path = apc.encode_to(path, 'utf-8')
if(os.path.isdir(path)):
- res = api_client.remove_watched_dir(path)
- if(res is None):
+ try:
+ res = api_client.remove_watched_dir(path)
+ except Exception, e:
exit("Unable to connect to the Airtime server.")
# sucess
if(res['msg']['code'] == 0):
@@ -247,10 +253,11 @@ def StorageSetAction(option, opt, value, parser):
path = currentDir+path
path = apc.encode_to(path, 'utf-8')
if(os.path.isdir(path)):
- res = api_client.set_storage_dir(path)
- if(res is None):
+ try:
+ res = api_client.set_storage_dir(path)
+ except Exception, e:
exit("Unable to connect to the Airtime server.")
- # sucess
+ # success
if(res['msg']['code'] == 0):
print "Successfully set storage folder to %s" % path
else:
diff --git a/utils/airtime-silan b/utils/airtime-silan
new file mode 100755
index 000000000..efb96ef30
--- /dev/null
+++ b/utils/airtime-silan
@@ -0,0 +1,42 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# Copyright (c) 2011 Sourcefabric O.P.S.
+#
+# This file is part of the Airtime project.
+# http://airtime.sourcefabric.org/
+#
+# Airtime is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Airtime is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Airtime; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+#-------------------------------------------------------------------------------
+#-------------------------------------------------------------------------------
+# This script handles update cue-in/cue-out points for files that already exist
+# in Airtime's library.
+#
+exec 2>&1
+airtime_silan_script="airtime-silan.py"
+api_client_path="/usr/lib/airtime/"
+
+virtualenv_bin="/usr/lib/airtime/airtime_virtualenv/bin/"
+. ${virtualenv_bin}activate
+
+export PYTHONPATH=${api_client_path}
+
+# Absolute path to this script
+SCRIPT=`readlink -f $0`
+# Absolute directory this script is in
+SCRIPTPATH=`dirname $SCRIPT`
+
+cd $SCRIPTPATH
+python ${airtime_silan_script}
diff --git a/utils/airtime-silan/airtime-silan.py b/utils/airtime-silan.py
similarity index 84%
rename from utils/airtime-silan/airtime-silan.py
rename to utils/airtime-silan.py
index d5f691698..00e94d77e 100644
--- a/utils/airtime-silan/airtime-silan.py
+++ b/utils/airtime-silan.py
@@ -1,11 +1,10 @@
-import logging
+from configobj import ConfigObj
from api_clients import api_client as apc
+
+import logging
import json
-import shutil
-import commands
import os
import sys
-from configobj import ConfigObj
import subprocess
import traceback
@@ -19,16 +18,16 @@ logging.disable(50)
# add ch to logger
logger.addHandler(ch)
-if (os.geteuid() != 0):
+if os.geteuid() != 0:
print 'Must be a root user.'
- sys.exit()
+ sys.exit(1)
# loading config file
try:
config = ConfigObj('/etc/airtime/media-monitor.cfg')
except Exception, e:
print('Error loading config file: %s', e)
- sys.exit()
+ sys.exit(1)
api_client = apc.AirtimeApiClient(config)
@@ -49,26 +48,25 @@ try:
full_path = f['fp']
# silence detect(set default queue in and out)
try:
- command = ['silan', '-f', 'JSON', full_path]
+ command = ['silan', '-b' '-f', 'JSON', full_path]
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
- out = proc.stdout.read()
+ out = proc.communicate()[0].strip('\r\n')
info = json.loads(out)
data = {}
data['cuein'] = str('{0:f}'.format(info['sound'][0][0]))
data['cueout'] = str('{0:f}'.format(info['sound'][-1][1]))
processed_data.append((f['id'], data))
total += 1
- if (total % 5 == 0):
+ if total % 5 == 0:
print "Total %s / %s files has been processed.." % (total, total_files)
except Exception, e:
print e
print traceback.format_exc()
- break
print "Processed: %d songs" % total
subtotal += total
- total = 0
+
try:
- api_client.update_cue_values_by_silan(processed_data)
+ print api_client.update_cue_values_by_silan(processed_data)
except Exception ,e:
print e
print traceback.format_exc()
@@ -77,5 +75,3 @@ try:
except Exception, e:
print e
print traceback.format_exc()
-
-#update_cue_values_by_silan
diff --git a/utils/airtime-silan/airtime-silan b/utils/airtime-silan/airtime-silan
deleted file mode 100755
index be88f8954..000000000
--- a/utils/airtime-silan/airtime-silan
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-
-virtualenv_bin="/usr/lib/airtime/airtime_virtualenv/bin/"
-. ${virtualenv_bin}activate
-
-invokePwd=$PWD
-
-#airtime_silan_path="/usr/lib/airtime/utils/airtime-silan/"
-airtime_silan_path="/home/james/src/airtime/utils/airtime-silan/"
-airtime_silan_script="airtime-silan.py"
-
-api_client_path="/usr/lib/airtime/"
-cd ${airtime_silan_path}
-
-exec 2>&1
-
-export PYTHONPATH=${api_client_path}
-
-# Note the -u when calling python! we need it to get unbuffered binary stdout and stderr
-exec python -u ${airtime_silan_path}${airtime_silan_script} --dir "$invokePwd" "$@"
-
-# EOF