diff --git a/airtime_mvc/application/views/scripts/webstream/webstream.phtml b/airtime_mvc/application/views/scripts/webstream/webstream.phtml
index 1c3657a75..ee80adf30 100644
--- a/airtime_mvc/application/views/scripts/webstream/webstream.phtml
+++ b/airtime_mvc/application/views/scripts/webstream/webstream.phtml
@@ -1,3 +1,4 @@
+
");
}
- 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" },
@@ -560,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);
@@ -575,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;
});
@@ -611,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({
@@ -1270,6 +1356,8 @@ var validationTypes = {
"composer" : "s",
"conductor" : "s",
"copyright" : "s",
+ "cuein" : "l",
+ "cueout" : "l",
"encoded_by" : "s",
"utime" : "t",
"mtime" : "t",
@@ -1301,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/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 eec5e1c7e..50466f462 100644
--- a/airtime_mvc/public/js/airtime/preferences/preferences.js
+++ b/airtime_mvc/public/js/airtime/preferences/preferences.js
@@ -108,8 +108,7 @@ $(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);
diff --git a/airtime_mvc/public/js/airtime/preferences/streamsetting.js b/airtime_mvc/public/js/airtime/preferences/streamsetting.js
index c9d802ddb..01d4717b3 100644
--- a/airtime_mvc/public/js/airtime/preferences/streamsetting.js
+++ b/airtime_mvc/public/js/airtime/preferences/streamsetting.js
@@ -77,8 +77,7 @@ function showForIcecast(ele){
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
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/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/plugin/dataTables.columnFilter.js b/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js
index 07c8bba72..fbf333082 100644
--- a/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js
+++ b/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js
@@ -187,7 +187,7 @@
label = $.i18n._("kbps");
} else if (th.attr('id') == "utime" || th.attr('id') == "mtime" || th.attr('id') == "lptime") {
label = $.i18n._("yyyy-mm-dd");
- } else if (th.attr('id') == "length") {
+ } else if (th.attr('id') == "length" || th.attr('id') == "cuein" || th.attr('id') == "cueout") {
label = $.i18n._("hh:mm:ss.t");
} else if (th.attr('id') == "sample_rate") {
label = $.i18n._("kHz");
diff --git a/debian/changelog b/debian/changelog
index 3f9fcb53b..3eaa9a6b2 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+airtime (2.4.0-1) unstable; urgency=low
+
+ * Nightly development snapshot of Airtime 2.4.x series
+
+ -- Daniel James 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
diff --git a/debian/control b/debian/control
index 1bdec9e86..81ff7af73 100644
--- a/debian/control
+++ b/debian/control
@@ -43,7 +43,7 @@ Depends: apache2,
pwgen,
python,
rabbitmq-server,
- silan (>= 0.3.0~),
+ silan (>= 0.3.1~),
sudo,
sysv-rc,
tar (>= 1.22),
diff --git a/debian/postinst b/debian/postinst
index 2e410febe..b333fd46e 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -16,7 +16,7 @@ includefile="${configdir}/apache.conf"
a2tplfile="${configdir}/apache.vhost.tpl"
phpinifile="${configdir}/airtime.ini"
OLDVERSION="$2"
-NEWVERSION="2.3.1"
+NEWVERSION="2.4.0"
case "$1" in
configure|reconfigure)
diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py
index 13b2e6589..4dc685ba6 100644
--- a/python_apps/api_clients/api_client.py
+++ b/python_apps/api_clients/api_client.py
@@ -116,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%%'))
@@ -247,7 +249,9 @@ class AirtimeApiClient(object):
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])
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/liquidsoap_scripts/ls_lib.liq b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq
index 5aa77cac8..ccb37026f 100644
--- a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq
+++ b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq
@@ -354,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 1df261316..1add69009 100644
--- a/python_apps/pypo/liquidsoap_scripts/ls_script.liq
+++ b/python_apps/pypo/liquidsoap_scripts/ls_script.liq
@@ -34,8 +34,35 @@ just_switched = ref false
%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
@@ -247,7 +274,7 @@ end
# 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/pypocli.py b/python_apps/pypo/pypocli.py
index 941f3610a..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
@@ -63,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))
@@ -229,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 7c50b4e2d..07ffcf2bf 100644
--- a/python_apps/pypo/pypofetch.py
+++ b/python_apps/pypo/pypofetch.py
@@ -7,17 +7,18 @@ import logging.config
import json
import telnetlib
import copy
-from threading import Thread
import subprocess
+import signal
+from datetime import datetime
import traceback
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 subprocess import Popen, PIPE
-
-from configobj import ConfigObj
# configure logging
logging_cfg = os.path.join(os.path.dirname(__file__), "logging.cfg")
@@ -25,6 +26,12 @@ 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)
@@ -35,8 +42,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()
@@ -481,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'):
@@ -490,12 +496,17 @@ 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
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/pypopush.py b/python_apps/pypo/pypopush.py
index cd552cf52..4d6c21964 100644
--- a/python_apps/pypo/pypopush.py
+++ b/python_apps/pypo/pypopush.py
@@ -9,10 +9,14 @@ import logging.config
import telnetlib
import calendar
import math
+import traceback
import os
-from pypofetch import PypoFetch
-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
@@ -36,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()
@@ -58,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:
@@ -159,289 +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):
- file_chain = self.modify_first_link_cue_point(file_chain)
- 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:
- chain_to_push = self.modify_first_link_cue_point(chain_to_push)
- 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
-
- def modify_first_link_cue_point(self, chain):
- if not len(chain):
- return []
-
- first_link = chain[0]
-
- self.modify_cue_point(first_link)
-
- #ATM, we should never have an exception here. However in the future, it
- #would be nice to allow cue_out to be None which would then allow us to
- #fallback to the length of the track as the end point.
- try:
- end = first_link['cue_out']
- except TypeError:
- #cue_out is type None
- end = first_link['length']
-
- if float(first_link['cue_in']) >= float(end):
- chain = chain [1:]
-
- return chain
-
- """
- 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):
"""
@@ -450,101 +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)
-
- 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())
-
- 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()
@@ -571,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 70d99144d..b3818f32d 100644
--- a/python_apps/pypo/recorder.py
+++ b/python_apps/pypo/recorder.py
@@ -303,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/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
+