Merge branch '2.2.x' into 2.2.x-saas

Conflicts:
	airtime_mvc/application/forms/LiveStreamingPreferences.php
This commit is contained in:
Martin Konecny 2012-11-28 16:10:33 -05:00
commit 4983721565
27 changed files with 385 additions and 246 deletions

View File

@ -1,2 +1,2 @@
PRODUCT_ID=Airtime PRODUCT_ID=Airtime
PRODUCT_RELEASE=2.2.0 PRODUCT_RELEASE=2.2.1

View File

@ -952,7 +952,9 @@ class ApiController extends Zend_Controller_Action
$data_arr = json_decode($data); $data_arr = json_decode($data);
if (!is_null($media_id) && isset($data_arr->title) && strlen($data_arr->title) < 1024) { if (!is_null($media_id)) {
if (isset($data_arr->title) &&
strlen($data_arr->title) < 1024) {
$previous_metadata = CcWebstreamMetadataQuery::create() $previous_metadata = CcWebstreamMetadataQuery::create()
->orderByDbStartTime('desc') ->orderByDbStartTime('desc')
@ -974,9 +976,9 @@ class ApiController extends Zend_Controller_Action
$webstream_metadata->setDbLiquidsoapData($data_arr->title); $webstream_metadata->setDbLiquidsoapData($data_arr->title);
$webstream_metadata->save(); $webstream_metadata->save();
} }
}
} else { } else {
throw new Error("Unexpected error. media_id $media_id has a null stream value in cc_schedule!"); throw new Exception("Null value of media_id");
} }
$this->view->response = $data; $this->view->response = $data;

View File

@ -107,7 +107,6 @@ class Application_Form_LiveStreamingPreferences extends Zend_Form_SubForm
public function isValid($data) public function isValid($data)
{ {
$isValid = parent::isValid($data); $isValid = parent::isValid($data);
return $isValid; return $isValid;
} }

View File

@ -360,8 +360,10 @@ SQL;
{ {
$sql = <<<SQL $sql = <<<SQL
SELECT SUM(cliplength) AS LENGTH SELECT SUM(cliplength) AS LENGTH
FROM cc_blockcontents FROM cc_blockcontents as bc
JOIN cc_files as f ON bc.file_id = f.id
WHERE block_id = :block_id WHERE block_id = :block_id
AND f.file_exists = true
SQL; SQL;
$result = Application_Common_Database::prepareAndExecute($sql, array(':block_id'=>$this->id), 'all', PDO::FETCH_NUM); $result = Application_Common_Database::prepareAndExecute($sql, array(':block_id'=>$this->id), 'all', PDO::FETCH_NUM);
return $result[0][0]; return $result[0][0];
@ -473,7 +475,7 @@ SQL;
} }
foreach ($p_items as $ac) { foreach ($p_items as $ac) {
Logging::info("Adding audio file {$ac}"); Logging::info("Adding audio file {$ac[0]}");
try { try {
if (is_array($ac) && $ac[1] == 'audioclip') { if (is_array($ac) && $ac[1] == 'audioclip') {
$res = $this->insertBlockElement($this->buildEntry($ac[0], $pos)); $res = $this->insertBlockElement($this->buildEntry($ac[0], $pos));

View File

@ -204,9 +204,9 @@ class Application_Model_Preference
$fade = $out; $fade = $out;
} }
$fade = number_format($fade, 2); $fade = number_format($fade, 1);
//fades need 2 leading zeros for DateTime conversion //fades need 2 leading zeros for DateTime conversion
$fade = rtrim(str_pad($fade, 5, "0", STR_PAD_LEFT), "0"); $fade = str_pad($fade, 4, "0", STR_PAD_LEFT);
return $fade; return $fade;
} }
@ -1109,7 +1109,6 @@ class Application_Model_Preference
} else { } else {
/*For now we just have this hack for debugging. We should not /*For now we just have this hack for debugging. We should not
rely on this crappy behaviour in case of failure*/ rely on this crappy behaviour in case of failure*/
Logging::info("Pref: $pref_param");
Logging::warn("Index $x does not exist preferences"); Logging::warn("Index $x does not exist preferences");
Logging::warn("Defaulting to identity and printing preferences"); Logging::warn("Defaulting to identity and printing preferences");
Logging::warn($ds); Logging::warn($ds);

View File

@ -321,7 +321,7 @@ SQL;
ws.description AS file_album_title, ws.description AS file_album_title,
ws.length AS file_length, ws.length AS file_length,
't'::BOOL AS file_exists, 't'::BOOL AS file_exists,
NULL as file_mime ws.mime as file_mime
SQL; SQL;
$streamJoin = <<<SQL $streamJoin = <<<SQL
cc_schedule AS sched cc_schedule AS sched
@ -674,6 +674,12 @@ SQL;
$start = self::AirtimeTimeToPypoTime($item["start"]); $start = self::AirtimeTimeToPypoTime($item["start"]);
$end = self::AirtimeTimeToPypoTime($item["end"]); $end = self::AirtimeTimeToPypoTime($item["end"]);
list(,,,$start_hour,,) = explode("-", $start);
list(,,,$end_hour,,) = explode("-", $end);
$same_hour = $start_hour == $end_hour;
$independent_event = !$same_hour;
$schedule_item = array( $schedule_item = array(
'id' => $media_id, 'id' => $media_id,
'type' => 'file', 'type' => 'file',
@ -687,7 +693,7 @@ SQL;
'end' => $end, 'end' => $end,
'show_name' => $item["show_name"], 'show_name' => $item["show_name"],
'replay_gain' => is_null($item["replay_gain"]) ? "0": $item["replay_gain"], 'replay_gain' => is_null($item["replay_gain"]) ? "0": $item["replay_gain"],
'independent_event' => false 'independent_event' => $independent_event,
); );
self::appendScheduleItem($data, $start, $schedule_item); self::appendScheduleItem($data, $start, $schedule_item);
} }
@ -828,25 +834,59 @@ SQL;
} }
} }
/** /* Check if two events are less than or equal to 1 second apart
* Purpose of this function is to iterate through the entire
* schedule array that was just built and fix the data up a bit. For
* example, if we have two consecutive webstreams, we don't need the
* first webstream to shutdown the output, when the second one will
* just switch it back on. Preventing this behaviour stops hiccups
* in output sound.
*/ */
private static function filterData(&$data) public static function areEventsLinked($event1, $event2) {
$dt1 = DateTime::createFromFormat("Y-m-d-H-i-s", $event1['start']);
$dt2 = DateTime::createFromFormat("Y-m-d-H-i-s", $event2['start']);
$seconds = $dt2->getTimestamp() - $dt1->getTimestamp();
return $seconds <= 1;
}
/**
* Streams are a 4 stage process.
* 1) start buffering stream 5 seconds ahead of its start time
* 2) at the start time tell liquidsoap to switch to this source
* 3) at the end time, tell liquidsoap to stop reading this stream
* 4) at the end time, tell liquidsoap to switch away from input.http source.
*
* When we have two streams back-to-back, some of these steps are unnecessary
* for the second stream. Instead of sending commands 1,2,3,4,1,2,3,4 we should
* send 1,2,1,2,3,4 - We don't need to tell liquidsoap to stop reading (#3), because #1
* of the next stream implies this when we pass in a new url. We also don't need #4.
*
* There's a special case here is well. When the back-to-back streams are the same, we
* can collapse the instructions 1,2,(3,4,1,2),3,4 to 1,2,3,4. We basically cut out the
* middle part. This function handles this.
*/
private static function foldData(&$data)
{ {
$previous_key = null; $previous_key = null;
$previous_val = null; $previous_val = null;
$previous_previous_key = null;
$previous_previous_val = null;
$previous_previous_previous_key = null;
$previous_previous_previous_val = null;
foreach ($data as $k => $v) { foreach ($data as $k => $v) {
if ($v["type"] == "stream_buffer_start"
&& !is_null($previous_val)
&& $previous_val["type"] == "stream_output_end") {
if ($v["type"] == "stream_output_start"
&& !is_null($previous_previous_val)
&& $previous_previous_val["type"] == "stream_output_end"
&& self::areEventsLinked($previous_previous_val, $v)) {
unset($data[$previous_previous_previous_key]);
unset($data[$previous_previous_key]);
unset($data[$previous_key]); unset($data[$previous_key]);
if ($previous_previous_val['uri'] == $v['uri']) {
unset($data[$k]);
} }
}
$previous_previous_previous_key = $previous_previous_key;
$previous_previous_previous_val = $previous_previous_val;
$previous_previous_key = $previous_key;
$previous_previous_val = $previous_val;
$previous_key = $k; $previous_key = $k;
$previous_val = $v; $previous_val = $v;
} }
@ -859,10 +899,12 @@ SQL;
$data = array(); $data = array();
$data["media"] = array(); $data["media"] = array();
//Harbor kick times *MUST* be ahead of schedule events, so that pypo
//executes them first.
self::createInputHarborKickTimes($data, $range_start, $range_end); self::createInputHarborKickTimes($data, $range_start, $range_end);
self::createScheduledEvents($data, $range_start, $range_end); self::createScheduledEvents($data, $range_start, $range_end);
self::filterData($data["media"]); self::foldData($data["media"]);
return $data; return $data;
} }

View File

@ -267,7 +267,11 @@ class Application_Model_ShowBuilder
$row["starts"] = $schedStartDT->format("H:i:s"); $row["starts"] = $schedStartDT->format("H:i:s");
$row["ends"] = $schedEndDT->format("H:i:s"); $row["ends"] = $schedEndDT->format("H:i:s");
$formatter = new LengthFormatter($p_item['file_length']); $cue_out = Application_Common_DateHelper::calculateLengthInSeconds($p_item['cue_out']);
$cue_in = Application_Common_DateHelper::calculateLengthInSeconds($p_item['cue_in']);
$run_time = $cue_out-$cue_in;
$formatter = new LengthFormatter(Application_Common_DateHelper::ConvertMSToHHMMSSmm($run_time*1000));
$row['runtime'] = $formatter->format(); $row['runtime'] = $formatter->format();
$row["title"] = $p_item["file_track_title"]; $row["title"] = $p_item["file_track_title"];

View File

@ -34,7 +34,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
public function getCreatorId() public function getCreatorId()
{ {
return $this->Webstream->getCcSubjs()->getDbId(); return $this->webstream->getDbCreatorId();
} }
public function getLastModified($p_type) public function getLastModified($p_type)

View File

@ -153,12 +153,24 @@ function buildplaylist(p_url, p_playIndex) {
continue; continue;
} }
} else if (data[index]['type'] == 1) { } else if (data[index]['type'] == 1) {
media = {title: data[index]['element_title'], var mime = data[index]['mime'];
artist: data[index]['element_artist'], if (mime.search(/mp3/i) > 0 || mime.search(/mpeg/i) > 0) {
mp3:data[index]['uri'] key = "mp3";
}; } else if (mime.search(/og(g|a)/i) > 0 || mime.search(/vorbis/i) > 0) {
key = "oga";
} else if (mime.search(/mp4/i) > 0) {
key = "m4a";
} else if (mime.search(/wav/i) > 0) {
key = "wav";
}
if (key) {
media = {title: data[index]['element_title'],
artist: data[index]['element_artist']
};
media[key] = data[index]['uri']
}
} }
console.log(data[index]);
if (media && isAudioSupported(data[index]['mime'])) { if (media && isAudioSupported(data[index]['mime'])) {
// javascript doesn't support associative array with numeric key // javascript doesn't support associative array with numeric key
// so we need to remove the gap if we skip any of tracks due to // so we need to remove the gap if we skip any of tracks due to

View File

@ -15,5 +15,6 @@ function isAudioSupported(mime){
//is adding a javascript library to do the work for you, which seems like overkill.... //is adding a javascript library to do the work for you, which seems like overkill....
return (!!audio.canPlayType && audio.canPlayType(bMime) != "") || return (!!audio.canPlayType && audio.canPlayType(bMime) != "") ||
(mime.indexOf("mp3") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined) || (mime.indexOf("mp3") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined) ||
(mime.indexOf("mp4") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined); (mime.indexOf("mp4") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined) ||
(mime.indexOf("mpeg") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined);
} }

View File

@ -81,6 +81,10 @@ var AIRTIME = (function(AIRTIME) {
return container; return container;
}, },
cursor : 'pointer', cursor : 'pointer',
cursorAt: {
top: 30,
left: 100
},
connectToSortable : '#show_builder_table' connectToSortable : '#show_builder_table'
}); });
}; };

View File

@ -975,7 +975,6 @@ function addProgressIcon(id) {
} }
function checkLibrarySCUploadStatus(){ function checkLibrarySCUploadStatus(){
var url = '/Library/get-upload-to-soundcloud-status', var url = '/Library/get-upload-to-soundcloud-status',
span, span,
id; id;
@ -1028,15 +1027,22 @@ function addQtipToSCIcons(){
}); });
} }
else if($(this).hasClass("soundcloud")){ else if($(this).hasClass("soundcloud")){
var sc_id = $(this).parent().parent().data("aData").soundcloud_id;
$(this).qtip({ $(this).qtip({
content: { content: {
//text: "The soundcloud id for this file is: "+sc_id
text: "Retrieving data from the server...", text: "Retrieving data from the server...",
ajax: { ajax: {
url: "/Library/get-upload-to-soundcloud-status", url: "/Library/get-upload-to-soundcloud-status",
type: "post", type: "post",
data: ({format: "json", id : id, type: "file"}), data: ({format: "json", id : id, type: "file"}),
success: function(json, status){ success: function(json, status){
this.set('content.text', "The soundcloud id for this file is: "+json.sc_id); id = sc_id;
if (id == undefined) {
id = json.sc_id;
}
this.set('content.text', "The soundcloud id for this file is: "+id);
} }
} }
}, },

View File

@ -402,8 +402,9 @@ function setupUI() {
$(".repeat_tracks_help_icon").qtip({ $(".repeat_tracks_help_icon").qtip({
content: { content: {
text: "If your criteria is too strict, Airtime may not be able to fill up the desired smart block length." + text: "The desired block length will not be reached if Airtime cannot find " +
" Hence, if you check this option, tracks will be used more than once." "enough unique tracks to match your criteria. Enable this option if you wish to allow " +
"tracks to be added multiple times to the smart block."
}, },
hide: { hide: {
delay: 500, delay: 500,

View File

@ -25,7 +25,8 @@ echo "----------------------------------------------------"
dist=`lsb_release -is` dist=`lsb_release -is`
code=`lsb_release -cs` code=`lsb_release -cs`
if [ "$dist" = "Debian" ]; then #enable squeeze backports to get lame packages
if [ "$dist" = "Debian" -a "$code" = "squeeze" ]; then
set +e set +e
grep -E "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list grep -E "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list
returncode=$? returncode=$?

View File

@ -28,7 +28,8 @@ echo "----------------------------------------------------"
dist=`lsb_release -is` dist=`lsb_release -is`
code=`lsb_release -cs` code=`lsb_release -cs`
if [ "$dist" -eq "Debian" ]; then #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 grep "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list
if [ "$?" -ne "0" ]; then if [ "$?" -ne "0" ]; then
echo "deb http://backports.debian.org/debian-backports squeeze-backports main" >> /etc/apt/sources.list echo "deb http://backports.debian.org/debian-backports squeeze-backports main" >> /etc/apt/sources.list

View File

@ -1,3 +1,3 @@
<?php <?php
define('AIRTIME_VERSION', '2.2.0'); define('AIRTIME_VERSION', '2.2.1');

View File

@ -100,4 +100,8 @@ if (strcmp($version, "2.2.0") < 0) {
passthru("php --php-ini $SCRIPTPATH/../airtime-php.ini $SCRIPTPATH/../upgrades/airtime-2.2.0/airtime-upgrade.php"); passthru("php --php-ini $SCRIPTPATH/../airtime-php.ini $SCRIPTPATH/../upgrades/airtime-2.2.0/airtime-upgrade.php");
pause(); pause();
} }
if (strcmp($version, "2.2.1") < 0) {
passthru("php --php-ini $SCRIPTPATH/../airtime-php.ini $SCRIPTPATH/../upgrades/airtime-2.2.1/airtime-upgrade.php");
pause();
}
echo "******************************* Upgrade Complete *******************************".PHP_EOL; echo "******************************* Upgrade Complete *******************************".PHP_EOL;

View File

@ -0,0 +1,24 @@
<?php
/* All functions other than start() should be marked as
* private.
*/
class AirtimeDatabaseUpgrade{
public static function start($p_dbValues){
echo "* Updating Database".PHP_EOL;
self::task0($p_dbValues);
echo " * Complete".PHP_EOL;
}
private static function task0($p_dbValues){
$username = $p_dbValues['database']['dbuser'];
$password = $p_dbValues['database']['dbpass'];
$host = $p_dbValues['database']['host'];
$database = $p_dbValues['database']['dbname'];
$dir = __DIR__;
passthru("export PGPASSWORD=$password && psql -h $host -U $username -q -f $dir/data/upgrade.sql $database 2>&1 | grep -v \"will create implicit index\"");
}
}

View File

@ -0,0 +1,8 @@
<?php
require_once 'DbUpgrade.php';
$filename = "/etc/airtime/airtime.conf";
$values = parse_ini_file($filename, true);
AirtimeDatabaseUpgrade::start($values);

View File

@ -0,0 +1,9 @@
DELETE FROM cc_pref WHERE keystr = 'system_version';
INSERT INTO cc_pref (keystr, valstr) VALUES ('system_version', '2.2.1');
ALTER TABLE cc_block
DROP CONSTRAINT cc_block_createdby_fkey;
ALTER TABLE cc_block
ADD CONSTRAINT cc_block_createdby_fkey FOREIGN KEY (creator_id) REFERENCES cc_subjs(id) ON DELETE CASCADE;

View File

@ -18,7 +18,7 @@ from configobj import ConfigObj
import string import string
import traceback import traceback
AIRTIME_VERSION = "2.2.0" AIRTIME_VERSION = "2.2.1"
# TODO : Place these functions in some common module. Right now, media # TODO : Place these functions in some common module. Right now, media

View File

@ -255,28 +255,14 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de
user_ref := "source" user_ref := "source"
end end
description_ref = ref description
if description == "" then
description_ref := "N/A"
end
genre_ref = ref genre
if genre == "" then
genre_ref := "N/A"
end
url_ref = ref url
if url == "" then
url_ref := "N/A"
end
output.shoutcast_mono = output.shoutcast(id = "shoutcast_stream_#{stream}", output.shoutcast_mono = output.shoutcast(id = "shoutcast_stream_#{stream}",
host = host, host = host,
port = port, port = port,
password = pass, password = pass,
fallible = true, fallible = true,
url = !url_ref, url = url,
genre = !genre_ref, genre = genre,
name = !description_ref, name = description,
user = !user_ref, user = !user_ref,
on_error = on_error, on_error = on_error,
on_connect = on_connect) on_connect = on_connect)
@ -286,9 +272,9 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de
port = port, port = port,
password = pass, password = pass,
fallible = true, fallible = true,
url = !url_ref, url = url,
genre = !genre_ref, genre = genre,
name = !description_ref, name = description,
user = !user_ref, user = !user_ref,
on_error = on_error, on_error = on_error,
on_connect = on_connect) on_connect = on_connect)
@ -390,13 +376,6 @@ def add_skip_command(s)
"skip",fun(s) -> begin log("source.skip") skip(s) end) "skip",fun(s) -> begin log("source.skip") skip(s) end)
end end
dyn_out = output.icecast(%wav,
host="localhost",
port=8999,
password=stream_harbor_pass,
mount="test-harbor",
fallible=true)
def set_dynamic_source_id(id) = def set_dynamic_source_id(id) =
current_dyn_id := id current_dyn_id := id
string_of(!current_dyn_id) string_of(!current_dyn_id)
@ -406,123 +385,159 @@ def get_dynamic_source_id() =
string_of(!current_dyn_id) string_of(!current_dyn_id)
end end
#cc-4633
# Function to create a playlist source and output it.
def create_dynamic_source(uri) =
# The playlist source
s = audio_to_stereo(input.http(buffer=2., max=12., uri))
# The output # NOTE
active_dyn_out = dyn_out(s) # A few values are hardcoded and may be dependent:
# - the delay in gracetime is linked with the buffer duration of input.http
# (delay should be a bit less than buffer)
# - crossing duration should be less than buffer length
# (at best, a higher duration will be ineffective)
# We register both source and output # HTTP input with "restart" command that waits for "stop" to be effected
# in the list of sources # before "start" command is issued. Optionally it takes a new URL to play,
dyn_sources := # which makes it a convenient replacement for "url".
list.append([(!current_dyn_id, s),(!current_dyn_id, active_dyn_out)], !dyn_sources) # In the future, this may become a core feature of the HTTP input.
# TODO If we stop and restart quickly several times in a row,
# the data bursts accumulate and create buffer overflow.
# Flushing the buffer on restart could be a good idea, but
# it would also create an interruptions while the buffer is
# refilling... on the other hand, this would avoid having to
# fade using both cross() and switch().
def input.http_restart(~id,~initial_url="http://dummy/url")
notify([("schedule_table_id", !current_dyn_id)]) source = input.http(buffer=5.,max=15.,id=id,autostart=false,initial_url)
"Done!"
def stopped()
"stopped" == list.hd(server.execute("#{id}.status"))
end end
server.register(namespace=id,
"restart",
usage="restart [url]",
fun (url) -> begin
if url != "" then
log(string_of(server.execute("#{id}.url #{url}")))
end
log(string_of(server.execute("#{id}.stop")))
add_timeout(0.5,
{ if stopped() then
log(string_of(server.execute("#{id}.start"))) ;
(-1.)
else 0.5 end})
"OK"
end)
# A function to destroy a dynamic source # Dummy output should be useless if HTTP stream is meant
def destroy_dynamic_source(id) = # to be listened to immediately. Otherwise, apply it.
# We need to find the source in the list, #
# remove it and destroy it. Currently, the language # output.dummy(fallible=true,source)
# lacks some nice operators for that so we do it
# the functional way
# This function is executed on every item in the list source
# of dynamic sources
def parse_list(ret, current_element) =
# ret is of the form: (matching_sources, remaining_sources)
# We extract those two:
matching_sources = fst(ret)
remaining_sources = snd(ret)
# current_element is of the form: ("uri", source) so end
# we check the first element
current_id = fst(current_element) # Transitions between URL changes in HTTP streams.
if current_id == id then def cross_http(~debug=true,~http_input_id,source)
# In this case, we add the source to the list of
# matched sources id = http_input_id
(list.append( [snd(current_element)], last_url = ref ""
matching_sources), change = ref false
remaining_sources)
def on_m(m)
notify_stream(m)
changed = m["source_url"] != !last_url
log("URL now #{m['source_url']} (change: #{changed})")
if changed then
if !last_url != "" then change := true end
last_url := m["source_url"]
end
end
# We use both metadata and status to know about the current URL.
# Using only metadata may be more precise is crazy corner cases,
# but it's also asking too much: the metadata may not pass through
# before the crosser is instantiated.
# Using only status in crosser misses some info, eg. on first URL.
source = on_metadata(on_m,source)
cross_d = 3.
def crosser(a,b)
url = list.hd(server.execute('#{id}.url'))
status = list.hd(server.execute('#{id}.status'))
on_m([("source_url",url)])
if debug then
log("New track inside HTTP stream")
log(" status: #{status}")
log(" need to cross: #{!change}")
log(" remaining #{source.remaining(a)} sec before, \
#{source.remaining(b)} sec after")
end
if !change then
change := false
# In principle one should avoid crossing on a live stream
# it'd be okay to do it here (eg. use add instead of sequence)
# because it's only once per URL, but be cautious.
sequence([fade.out(duration=cross_d,a),fade.in(b)])
else else
# In this case, we put the element in the list of remaining # This is done on tracks inside a single stream.
# sources # Do NOT cross here or you'll gradually empty the buffer!
(matching_sources, sequence([a,b])
list.append([current_element],
remaining_sources))
end end
end end
# Now we execute the function: # Setting conservative=true would mess with the delayed switch below
result = list.fold(parse_list, ([], []), !dyn_sources) cross(duration=cross_d,conservative=false,crosser,source)
matching_sources = fst(result)
remaining_sources = snd(result)
# We store the remaining sources in dyn_sources end
dyn_sources := remaining_sources
# If no source matched, we return an error # Custom fallback between http and default source with fading of
if list.length(matching_sources) == 0 then # beginning and end of HTTP stream.
"Error: no matching sources!" # It does not take potential URL changes into account, as long as
# they do not interrupt streaming (thanks to the HTTP buffer).
def http_fallback(~http_input_id,~http,~default)
id = http_input_id
# We use a custom switching predicate to trigger switching (and thus,
# transitions) before the end of a track (rather, end of HTTP stream).
# It is complexified because we don't want to trigger switching when
# HTTP disconnects for just an instant, when changing URL: for that
# we use gracetime below.
def gracetime(~delay=3.,f)
last_true = ref 0.
{ if f() then
last_true := gettimeofday()
true
else else
# We stop all sources gettimeofday() < !last_true+delay
list.iter(source.shutdown, matching_sources) end }
# And return
"Done!"
end
end end
def connected()
status = list.hd(server.execute("#{id}.status"))
not(list.mem(status,["polling","stopped"]))
end
connected = gracetime(connected)
def to_live(a,b) =
log("TRANSITION to live")
# A function to destroy a dynamic source add(normalize=false,
def destroy_dynamic_source_all() = [fade.initial(b),fade.final(a)])
# We need to find the source in the list, end
# remove it and destroy it. Currently, the language def to_static(a,b) =
# lacks some nice operators for that so we do it log("TRANSITION to static")
# the functional way sequence([fade.out(a),fade.initial(b)])
# This function is executed on every item in the list
# of dynamic sources
def parse_list(ret, current_element) =
# ret is of the form: (matching_sources, remaining_sources)
# We extract those two:
matching_sources = fst(ret)
remaining_sources = snd(ret)
# current_element is of the form: ("uri", source) so
# we check the first element
current_uri = fst(current_element)
# in this case, we add the source to the list of
# matched sources
(list.append( [snd(current_element)],
matching_sources),
remaining_sources)
end end
# now we execute the function: switch(
result = list.fold(parse_list, ([], []), !dyn_sources) track_sensitive=false,
matching_sources = fst(result) transitions=[to_live,to_static],
remaining_sources = snd(result) [(# make sure it is connected, and not buffering
{connected() and source.is_ready(http) and !webstream_enabled}, http),
({true},default)])
# we store the remaining sources in dyn_sources
dyn_sources := remaining_sources
# if no source matched, we return an error
if list.length(matching_sources) == 0 then
"error: no matching sources!"
else
# we stop all sources
list.iter(source.shutdown, matching_sources)
# And return
"Done!"
end end
end

View File

@ -7,15 +7,11 @@ set("server.telnet", true)
set("server.telnet.port", 1234) set("server.telnet.port", 1234)
#Dynamic source list #Dynamic source list
dyn_sources = ref [] #dyn_sources = ref []
webstream_enabled = ref false webstream_enabled = ref false
time = ref string_of(gettimeofday()) time = ref string_of(gettimeofday())
queue = audio_to_stereo(id="queue_src", request.equeue(id="queue", length=0.5))
queue = cue_cut(queue)
queue = amplify(1., override="replay_gain", queue)
#fallback between queue and input.harbor (for restreaming other web-streams)
#live stream setup #live stream setup
set("harbor.bind_addr", "0.0.0.0") set("harbor.bind_addr", "0.0.0.0")
@ -35,14 +31,17 @@ s2_namespace = ref ''
s3_namespace = ref '' s3_namespace = ref ''
just_switched = ref false just_switched = ref false
stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20')) #stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20'))
%include "ls_lib.liq" %include "ls_lib.liq"
web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass) #web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass)
web_stream = on_metadata(notify_stream, web_stream) #web_stream = on_metadata(notify_stream, web_stream)
output.dummy(fallible=true, web_stream) #output.dummy(fallible=true, web_stream)
queue = audio_to_stereo(id="queue_src", request.equeue(id="queue", length=0.5))
queue = cue_cut(queue)
queue = amplify(1., override="replay_gain", queue)
# the crossfade function controls fade in/out # the crossfade function controls fade in/out
queue = crossfade_airtime(queue) queue = crossfade_airtime(queue)
@ -50,11 +49,9 @@ queue = on_metadata(notify, queue)
queue = map_metadata(update=false, append_title, queue) queue = map_metadata(update=false, append_title, queue)
output.dummy(fallible=true, queue) output.dummy(fallible=true, queue)
http = input.http_restart(id="http")
stream_queue = switch(id="stream_queue_switch", track_sensitive=false, http = cross_http(http_input_id="http",http)
transitions=[transition, transition], stream_queue = http_fallback(http_input_id="http",http=http,default=queue)
[({!webstream_enabled},web_stream),
({true}, queue)])
ignore(output.dummy(stream_queue, fallible=true)) ignore(output.dummy(stream_queue, fallible=true))
@ -84,7 +81,9 @@ server.register(namespace="dynamic_source",
description="Enable webstream output", description="Enable webstream output",
usage='start', usage='start',
"output_start", "output_start",
fun (s) -> begin log("dynamic_source.output_start") webstream_enabled := true "enabled" end) 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", server.register(namespace="dynamic_source",
description="Enable webstream output", description="Enable webstream output",
usage='stop', usage='stop',
@ -92,32 +91,33 @@ server.register(namespace="dynamic_source",
fun (s) -> begin log("dynamic_source.output_stop") webstream_enabled := false "disabled" end) fun (s) -> begin log("dynamic_source.output_stop") webstream_enabled := false "disabled" end)
server.register(namespace="dynamic_source", server.register(namespace="dynamic_source",
description="Set the cc_schedule row id", description="Set the streams cc_schedule row id",
usage="id <id>", usage="id <id>",
"id", "id",
fun (s) -> begin log("dynamic_source.id") set_dynamic_source_id(s) end) fun (s) -> begin log("dynamic_source.id") set_dynamic_source_id(s) end)
server.register(namespace="dynamic_source", server.register(namespace="dynamic_source",
description="Get the cc_schedule row id", description="Get the streams cc_schedule row id",
usage="get_id", usage="get_id",
"get_id", "get_id",
fun (s) -> begin log("dynamic_source.get_id") get_dynamic_source_id() end) fun (s) -> begin log("dynamic_source.get_id") get_dynamic_source_id() end)
server.register(namespace="dynamic_source", #server.register(namespace="dynamic_source",
description="Start a new dynamic source.", # description="Start a new dynamic source.",
usage="start <uri>", # usage="start <uri>",
"read_start", # "read_start",
fun (uri) -> begin log("dynamic_source.read_start") create_dynamic_source(uri) end) # fun (uri) -> begin log("dynamic_source.read_start") begin_stream_read(uri) end)
server.register(namespace="dynamic_source", #server.register(namespace="dynamic_source",
description="Stop a dynamic source.", # description="Stop a dynamic source.",
usage="stop <id>", # usage="stop <id>",
"read_stop", # "read_stop",
fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source(s) end) # fun (s) -> begin log("dynamic_source.read_stop") stop_stream_read(s) end)
server.register(namespace="dynamic_source",
description="Stop a dynamic source.", #server.register(namespace="dynamic_source",
usage="stop <id>", # description="Stop a dynamic source.",
"read_stop_all", # usage="stop <id>",
fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end) # "read_stop_all",
# fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end)
default = amplify(id="silence_src", 0.00001, noise()) default = amplify(id="silence_src", 0.00001, noise())
default = rewrite_metadata([("artist","Airtime"), ("title", "offline")], default) default = rewrite_metadata([("artist","Airtime"), ("title", "offline")], default)
@ -224,6 +224,7 @@ end
s = switch(id="default_switch", track_sensitive=false, s = switch(id="default_switch", track_sensitive=false,
transitions=[transition_default, transition], transitions=[transition_default, transition],
[({!scheduled_play_enabled}, stream_queue),({true},default)]) [({!scheduled_play_enabled}, stream_queue),({true},default)])
s = append_dj_inputs(master_live_stream_port, master_live_stream_mp, s = append_dj_inputs(master_live_stream_port, master_live_stream_mp,
dj_live_stream_port, dj_live_stream_mp, s) dj_live_stream_port, dj_live_stream_mp, s)

View File

@ -478,8 +478,8 @@ class PypoPush(Thread):
self.logger.debug(msg) self.logger.debug(msg)
tn.write(msg) tn.write(msg)
#example: dynamic_source.read_start http://87.230.101.24:80/top100station.mp3 #msg = 'dynamic_source.read_start %s\n' % media_item['uri'].encode('latin-1')
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) self.logger.debug(msg)
tn.write(msg) tn.write(msg)
@ -520,7 +520,8 @@ class PypoPush(Thread):
self.telnet_lock.acquire() self.telnet_lock.acquire()
tn = telnetlib.Telnet(LS_HOST, LS_PORT) tn = telnetlib.Telnet(LS_HOST, LS_PORT)
msg = 'dynamic_source.read_stop_all xxx\n' #msg = 'dynamic_source.read_stop_all xxx\n'
msg = 'http.stop\n'
self.logger.debug(msg) self.logger.debug(msg)
tn.write(msg) tn.write(msg)
@ -546,7 +547,8 @@ class PypoPush(Thread):
tn = telnetlib.Telnet(LS_HOST, LS_PORT) tn = telnetlib.Telnet(LS_HOST, LS_PORT)
#dynamic_source.stop http://87.230.101.24:80/top100station.mp3 #dynamic_source.stop http://87.230.101.24:80/top100station.mp3
msg = 'dynamic_source.read_stop %s\n' % media_item['row_id'] #msg = 'dynamic_source.read_stop %s\n' % media_item['row_id']
msg = 'http.stop\n'
self.logger.debug(msg) self.logger.debug(msg)
tn.write(msg) tn.write(msg)

View File

@ -8,6 +8,9 @@ import json
import shutil import shutil
import commands import commands
sys.path.append('/usr/lib/airtime/media-monitor/mm2/')
from media.monitor.pure import is_file_supported
# create logger # create logger
logger = logging.getLogger() logger = logging.getLogger()
@ -53,8 +56,7 @@ def copy_or_move_files_to(paths, dest, flag):
copy_or_move_files_to(sub_path, dest, flag) copy_or_move_files_to(sub_path, dest, flag)
elif(os.path.isfile(path)): elif(os.path.isfile(path)):
#copy file to dest #copy file to dest
ext = os.path.splitext(path)[1] if(is_file_supported(path)):
if( 'mp3' in ext or 'ogg' in ext ):
destfile = dest+os.path.basename(path) destfile = dest+os.path.basename(path)
if(flag == 'copy'): if(flag == 'copy'):
print "Copying %(src)s to %(dest)s..." % {'src':path, 'dest':destfile} print "Copying %(src)s to %(dest)s..." % {'src':path, 'dest':destfile}
@ -159,7 +161,7 @@ def WatchAddAction(option, opt, value, parser):
path = currentDir+path path = currentDir+path
path = apc.encode_to(path, 'utf-8') path = apc.encode_to(path, 'utf-8')
if(os.path.isdir(path)): if(os.path.isdir(path)):
os.chmod(path, 0765) #os.chmod(path, 0765)
res = api_client.add_watched_dir(path) res = api_client.add_watched_dir(path)
if(res is None): if(res is None):
exit("Unable to connect to the server.") exit("Unable to connect to the server.")