Merge branch '2.2.x' of dev.sourcefabric.org:airtime into 2.2.x

This commit is contained in:
James 2012-10-29 16:19:39 -04:00
commit a937aa0e28
18 changed files with 467 additions and 748 deletions

View File

@ -25,66 +25,67 @@ class Application_Model_Soundcloud
public function uploadTrack($filepath, $filename, $description, public function uploadTrack($filepath, $filename, $description,
$tags=array(), $release=null, $genre=null) $tags=array(), $release=null, $genre=null)
{ {
if ($this->getToken()) {
if (count($tags)) {
$tags = join(" ", $tags);
$tags = $tags." ".Application_Model_Preference::GetSoundCloudTags();
} else {
$tags = Application_Model_Preference::GetSoundCloudTags();
}
$downloadable = Application_Model_Preference::GetSoundCloudDownloadbleOption() == '1'; if (!$this->getToken()) {
$track_data = array(
'track[sharing]' => 'private',
'track[title]' => $filename,
'track[asset_data]' => '@' . $filepath,
'track[tag_list]' => $tags,
'track[description]' => $description,
'track[downloadable]' => $downloadable,
);
if (isset($release)) {
$release = str_replace(" ", "-", $release);
$release = str_replace(":", "-", $release);
//YYYY-MM-DD-HH-mm-SS
$release = explode("-", $release);
$track_data['track[release_year]'] = $release[0];
$track_data['track[release_month]'] = $release[1];
$track_data['track[release_day]'] = $release[2];
}
if (isset($genre) && $genre != "") {
$track_data['track[genre]'] = $genre;
} else {
$default_genre = Application_Model_Preference::GetSoundCloudGenre();
if ($default_genre != "") {
$track_data['track[genre]'] = $default_genre;
}
}
$track_type = Application_Model_Preference::GetSoundCloudTrackType();
if ($track_type != "") {
$track_data['track[track_type]'] = $track_type;
}
$license = Application_Model_Preference::GetSoundCloudLicense();
if ($license != "") {
$track_data['track[license]'] = $license;
}
$response = json_decode(
$this->_soundcloud->post('tracks', $track_data),
true
);
return $response;
} else {
throw new NoSoundCloundToken(); throw new NoSoundCloundToken();
}
if (count($tags)) {
$tags = join(" ", $tags);
$tags = $tags." ".Application_Model_Preference::GetSoundCloudTags();
} else {
$tags = Application_Model_Preference::GetSoundCloudTags();
} }
$downloadable = Application_Model_Preference::GetSoundCloudDownloadbleOption() == '1';
$track_data = array(
'track[sharing]' => 'private',
'track[title]' => $filename,
'track[asset_data]' => '@' . $filepath,
'track[tag_list]' => $tags,
'track[description]' => $description,
'track[downloadable]' => $downloadable,
);
if (isset($release)) {
$release = str_replace(" ", "-", $release);
$release = str_replace(":", "-", $release);
//YYYY-MM-DD-HH-mm-SS
$release = explode("-", $release);
$track_data['track[release_year]'] = $release[0];
$track_data['track[release_month]'] = $release[1];
$track_data['track[release_day]'] = $release[2];
}
if (isset($genre) && $genre != "") {
$track_data['track[genre]'] = $genre;
} else {
$default_genre = Application_Model_Preference::GetSoundCloudGenre();
if ($default_genre != "") {
$track_data['track[genre]'] = $default_genre;
}
}
$track_type = Application_Model_Preference::GetSoundCloudTrackType();
if ($track_type != "") {
$track_data['track[track_type]'] = $track_type;
}
$license = Application_Model_Preference::GetSoundCloudLicense();
if ($license != "") {
$track_data['track[license]'] = $license;
}
$response = json_decode(
$this->_soundcloud->post('tracks', $track_data),
true
);
return $response;
} }
public static function uploadSoundcloud($id) public static function uploadSoundcloud($id)

View File

@ -92,5 +92,13 @@ if ($item['type'] == 2) {
<?php endforeach; ?> <?php endforeach; ?>
<?php else : ?> <?php else : ?>
<li class="spl_empty">Empty playlist</li> <li class="spl_empty">
<?php
if ($this->obj instanceof Application_Model_Block) {
echo 'Empty smart block';
} else {
echo 'Empty playlist';
}
?>
</li>
<?php endif; ?> <?php endif; ?>

View File

@ -339,11 +339,23 @@ var AIRTIME = (function(AIRTIME){
}); });
}; };
mod.jumpToCurrentTrack = function() {
var $scroll = $sbContent.find(".dataTables_scrolling");
var scrolled = $scroll.scrollTop();
var scrollingTop = $scroll.offset().top;
var oTable = $('#show_builder_table').dataTable();
var current = $sbTable.find("."+NOW_PLAYING_CLASS);
var currentTop = current.offset().top;
$scroll.scrollTop(currentTop - scrollingTop + scrolled);
}
mod.builderDataTable = function() { mod.builderDataTable = function() {
$sbContent = $('#show_builder'); $sbContent = $('#show_builder');
$lib = $("#library_content"), $lib = $("#library_content"),
$sbTable = $sbContent.find('table'); $sbTable = $sbContent.find('table');
var isInitialized = false;
oSchedTable = $sbTable.dataTable( { oSchedTable = $sbTable.dataTable( {
"aoColumns": [ "aoColumns": [
/* checkbox */ {"mDataProp": "allowed", "sTitle": "", "sWidth": "15px", "sClass": "sb-checkbox"}, /* checkbox */ {"mDataProp": "allowed", "sTitle": "", "sWidth": "15px", "sClass": "sb-checkbox"},
@ -636,6 +648,13 @@ var AIRTIME = (function(AIRTIME){
$("#draggingContainer").remove(); $("#draggingContainer").remove();
}, },
"fnDrawCallback": function fnBuilderDrawCallback(oSettings, json) { "fnDrawCallback": function fnBuilderDrawCallback(oSettings, json) {
if (!isInitialized) {
if ($(this).find("."+NOW_PLAYING_CLASS).length > 0) {
mod.jumpToCurrentTrack();
}
}
isInitialized = true;
var wrapperDiv, var wrapperDiv,
markerDiv, markerDiv,
$td, $td,
@ -1021,7 +1040,7 @@ var AIRTIME = (function(AIRTIME){
if (AIRTIME.button.isDisabled('icon-step-forward', true) === true) { if (AIRTIME.button.isDisabled('icon-step-forward', true) === true) {
return; return;
} }
/*
var $scroll = $sbContent.find(".dataTables_scrolling"), var $scroll = $sbContent.find(".dataTables_scrolling"),
scrolled = $scroll.scrollTop(), scrolled = $scroll.scrollTop(),
scrollingTop = $scroll.offset().top, scrollingTop = $scroll.offset().top,
@ -1029,6 +1048,8 @@ var AIRTIME = (function(AIRTIME){
currentTop = current.offset().top; currentTop = current.offset().top;
$scroll.scrollTop(currentTop - scrollingTop + scrolled); $scroll.scrollTop(currentTop - scrollingTop + scrolled);
*/
mod.jumpToCurrentTrack();
}); });
//delete overbooked tracks. //delete overbooked tracks.
@ -1196,7 +1217,7 @@ var AIRTIME = (function(AIRTIME){
}; };
} }
}); });
}; };
return AIRTIME; return AIRTIME;

View File

@ -113,174 +113,180 @@ AIRTIME = (function(AIRTIME) {
} }
mod.onReady = function() { mod.onReady = function() {
//define module vars. // define module vars.
$lib = $("#library_content"); $lib = $("#library_content");
$builder = $("#show_builder"); $builder = $("#show_builder");
$fs = $builder.find('fieldset'); $fs = $builder.find('fieldset');
/*
* Icon hover states for search.
*/
$builder.on("mouseenter", ".sb-timerange .ui-button", function(ev) {
$(this).addClass("ui-state-hover");
});
$builder.on("mouseleave", ".sb-timerange .ui-button", function(ev) {
$(this).removeClass("ui-state-hover");
});
$builder.find(dateStartId).datepicker(oBaseDatePickerSettings);
$builder.find(timeStartId).timepicker(oBaseTimePickerSettings);
$builder.find(dateEndId).datepicker(oBaseDatePickerSettings);
$builder.find(timeEndId).timepicker(oBaseTimePickerSettings);
oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId);
AIRTIME.showbuilder.fnServerData.start = oRange.start;
AIRTIME.showbuilder.fnServerData.end = oRange.end;
AIRTIME.library.libraryInit(); /*
AIRTIME.showbuilder.builderDataTable(); * Icon hover states for search.
setWidgetSize(); */
$builder.on("mouseenter", ".sb-timerange .ui-button", function(ev) {
$libWrapper = $lib.find("#library_display_wrapper"); $(this).addClass("ui-state-hover");
$libWrapper.prepend($libClose); });
$builder.on("mouseleave", ".sb-timerange .ui-button", function(ev) {
$builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 95); $(this).removeClass("ui-state-hover");
});
$builder.on("click", "#sb_submit", showSearchSubmit);
$builder.on("click","#sb_edit", function (ev){ $builder.find(dateStartId).datepicker(oBaseDatePickerSettings);
var schedTable = $("#show_builder_table").dataTable(); $builder.find(timeStartId).timepicker(oBaseTimePickerSettings);
$builder.find(dateEndId).datepicker(oBaseDatePickerSettings);
//reset timestamp to redraw the cursors. $builder.find(timeEndId).timepicker(oBaseTimePickerSettings);
AIRTIME.showbuilder.resetTimestamp();
oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId,
$lib.show() dateEndId, timeEndId);
.width(Math.floor(screenWidth * 0.48)); AIRTIME.showbuilder.fnServerData.start = oRange.start;
AIRTIME.showbuilder.fnServerData.end = oRange.end;
$builder.width(Math.floor(screenWidth * 0.48))
.find("#sb_edit") AIRTIME.library.libraryInit();
.remove() AIRTIME.showbuilder.builderDataTable();
.end() setWidgetSize();
.find("#sb_date_start")
.css("margin-left", 0) $libWrapper = $lib.find("#library_display_wrapper");
.end(); $libWrapper.prepend($libClose);
schedTable.fnDraw(); $builder.find('.dataTables_scrolling').css("max-height",
widgetHeight - 95);
$.ajax({
url: "/usersettings/set-now-playing-screen-settings", $builder.on("click", "#sb_submit", showSearchSubmit);
type: "POST",
data: {settings : {library : true}, format: "json"}, $builder.on("click", "#sb_edit", function(ev) {
dataType: "json", var schedTable = $("#show_builder_table").dataTable();
success: function(){}
}); // reset timestamp to redraw the cursors.
}); AIRTIME.showbuilder.resetTimestamp();
$lib.on("click", "#sb_lib_close", function() { $lib.show().width(Math.floor(screenWidth * 0.48));
var schedTable = $("#show_builder_table").dataTable();
$builder.width(Math.floor(screenWidth * 0.48)).find("#sb_edit")
.remove().end().find("#sb_date_start")
.css("margin-left", 0).end();
schedTable.fnDraw();
$.ajax( {
url : "/usersettings/set-now-playing-screen-settings",
type : "POST",
data : {
settings : {
library : true
},
format : "json"
},
dataType : "json",
success : function() {
}
});
});
$lib.on("click", "#sb_lib_close", function() {
var schedTable = $("#show_builder_table").dataTable();
$lib.hide();
$builder.width(screenWidth).find(".sb-timerange").prepend(
$toggleLib).find("#sb_date_start").css("margin-left", 30)
.end().end();
$toggleLib.removeClass("ui-state-hover");
schedTable.fnDraw();
$.ajax( {
url : "/usersettings/set-now-playing-screen-settings",
type : "POST",
data : {
settings : {
library : false
},
format : "json"
},
dataType : "json",
success : function() {
}
});
});
$builder.find('legend').click(
function(ev, item) {
if ($fs.hasClass("closed")) {
$fs.removeClass("closed");
$builder.find('.dataTables_scrolling').css(
"max-height", widgetHeight - 150);
} else {
$fs.addClass("closed");
// set defaults for the options.
$fs.find('select').val(0);
$fs.find('input[type="checkbox"]').attr("checked",
false);
$builder.find('.dataTables_scrolling').css(
"max-height", widgetHeight - 110);
}
});
// set click event for all my shows checkbox.
$builder.on("click", "#sb_my_shows", function(ev) {
if ($(this).is(':checked')) {
$(ev.delegateTarget).find('#sb_show_filter').val(0);
}
showSearchSubmit();
});
//set select event for choosing a show.
$builder.on("change", '#sb_show_filter', function(ev) {
if ($(this).val() !== 0) {
$(ev.delegateTarget).find('#sb_my_shows')
.attr("checked", false);
}
showSearchSubmit();
});
function checkScheduleUpdates() {
var data = {}, oTable = $('#show_builder_table').dataTable(), fn = oTable
.fnSettings().fnServerData, start = fn.start, end = fn.end;
data["format"] = "json";
data["start"] = start;
data["end"] = end;
data["timestamp"] = AIRTIME.showbuilder.getTimestamp();
data["instances"] = AIRTIME.showbuilder.getShowInstances();
if (fn.hasOwnProperty("ops")) {
data["myShows"] = fn.ops.myShows;
data["showFilter"] = fn.ops.showFilter;
}
$.ajax( {
"dataType" : "json",
"type" : "GET",
"url" : "/showbuilder/check-builder-feed",
"data" : data,
"success" : function(json) {
if (json.update === true) {
oTable.fnDraw();
}
}
});
}
//check if the timeline view needs updating.
setInterval(checkScheduleUpdates, 5 * 1000); //need refresh in milliseconds
};
mod.onResize = function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(setWidgetSize, 100);
};
return AIRTIME;
$lib.hide();
$builder.width(screenWidth)
.find(".sb-timerange")
.prepend($toggleLib)
.find("#sb_date_start")
.css("margin-left", 30)
.end()
.end();
$toggleLib.removeClass("ui-state-hover");
schedTable.fnDraw();
$.ajax({
url: "/usersettings/set-now-playing-screen-settings",
type: "POST",
data: {settings : {library : false}, format: "json"},
dataType: "json",
success: function(){}
});
});
$builder.find('legend').click(function(ev, item){
if ($fs.hasClass("closed")) {
$fs.removeClass("closed");
$builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 150);
}
else {
$fs.addClass("closed");
//set defaults for the options.
$fs.find('select').val(0);
$fs.find('input[type="checkbox"]').attr("checked", false);
$builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 110);
}
});
//set click event for all my shows checkbox.
$builder.on("click", "#sb_my_shows", function(ev) {
if ($(this).is(':checked')) {
$(ev.delegateTarget).find('#sb_show_filter').val(0);
}
showSearchSubmit();
});
//set select event for choosing a show.
$builder.on("change", '#sb_show_filter', function(ev) {
if ($(this).val() !== 0) {
$(ev.delegateTarget).find('#sb_my_shows').attr("checked", false);
}
showSearchSubmit();
});
function checkScheduleUpdates(){
var data = {},
oTable = $('#show_builder_table').dataTable(),
fn = oTable.fnSettings().fnServerData,
start = fn.start,
end = fn.end;
data["format"] = "json";
data["start"] = start;
data["end"] = end;
data["timestamp"] = AIRTIME.showbuilder.getTimestamp();
data["instances"] = AIRTIME.showbuilder.getShowInstances();
if (fn.hasOwnProperty("ops")) {
data["myShows"] = fn.ops.myShows;
data["showFilter"] = fn.ops.showFilter;
}
$.ajax( {
"dataType": "json",
"type": "GET",
"url": "/showbuilder/check-builder-feed",
"data": data,
"success": function(json) {
if (json.update === true) {
oTable.fnDraw();
}
}
} );
}
//check if the timeline view needs updating.
setInterval(checkScheduleUpdates, 5 * 1000); //need refresh in milliseconds
};
mod.onResize = function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(setWidgetSize, 100);
};
return AIRTIME;
} (AIRTIME || {})); } (AIRTIME || {}));
$(document).ready(AIRTIME.builderMain.onReady); $(document).ready(AIRTIME.builderMain.onReady);

View File

@ -67,7 +67,7 @@
* Removing a watched directory and adding it again preserves playlists & shows with those files. * Removing a watched directory and adding it again preserves playlists & shows with those files.
* An icon in the playlist shows whether a file is missing on disk, warning the user that the playlist will not go according to plan. * An icon in the playlist shows whether a file is missing on disk, warning the user that the playlist will not go according to plan.
* Media monitor detects add and removal of watched temporary local storage (USB disks for example) and network drives. * Media monitor detects add and removal of watched temporary local storage (USB disks for example) and network drives.
* Broadcast Log - export play count of tracks within a given time range.&nbsp; Useful for royalty reporting purposes. * Broadcast Log - export play count of tracks within a given time range. Useful for royalty reporting purposes.
* Minor Improvements: * Minor Improvements:
* Ability to turn off the broadcast. * Ability to turn off the broadcast.
* Editing metadata in the library will update the metadata on disk. * Editing metadata in the library will update the metadata on disk.
@ -78,7 +78,7 @@
* Repeating shows default to "No End" * Repeating shows default to "No End"
* Ability to "View on Soundcloud" for recorded shows in the calendar * Ability to "View on Soundcloud" for recorded shows in the calendar
* "Listen" preview player no longer falls behind the broadcast (you can only mute the stream now, not stop it) * "Listen" preview player no longer falls behind the broadcast (you can only mute the stream now, not stop it)
* Tracks that cannot be played will be rejected on upload and put in to the directory "/srv/airtime/store/problem_files" (but currently it will not tell you that it rejected them - sorry\!) * Tracks that cannot be played will be rejected on upload and put in to the directory "/srv/airtime/stor/problem_files" (but currently it will not tell you that it rejected them - sorry\!)
* Library is automatically refreshed when media import is finished * Library is automatically refreshed when media import is finished
* Show "Disk Full" message when trying to upload a file that wont fit on the disk * Show "Disk Full" message when trying to upload a file that wont fit on the disk
* Reduced CPU utilization for OGG streams * Reduced CPU utilization for OGG streams

View File

@ -181,7 +181,8 @@ libmad-ocaml-dev libtaglib-ocaml-dev libalsa-ocaml-dev libtaglib-ocaml-dev libvo
libspeex-dev libspeexdsp-dev speex libladspa-ocaml-dev festival festival-dev \ libspeex-dev libspeexdsp-dev speex libladspa-ocaml-dev festival festival-dev \
libsamplerate-dev libxmlplaylist-ocaml-dev libxmlrpc-light-ocaml-dev libflac-dev \ libsamplerate-dev libxmlplaylist-ocaml-dev libxmlrpc-light-ocaml-dev libflac-dev \
libxml-dom-perl libxml-dom-xpath-perl patch autoconf libmp3lame-dev \ libxml-dom-perl libxml-dom-xpath-perl patch autoconf libmp3lame-dev \
libcamomile-ocaml-dev libcamlimages-ocaml-dev libtool libpulse-dev libjack-dev camlidl libfaad-dev''') libcamomile-ocaml-dev libcamlimages-ocaml-dev libtool libpulse-dev libjack-dev
camlidl libfaad-dev libpcre-ocaml-dev''')
root = '/home/martin/src' root = '/home/martin/src'
do_run('mkdir -p %s' % root) do_run('mkdir -p %s' % root)

View File

@ -20,6 +20,11 @@ import traceback
AIRTIME_VERSION = "2.2.0" AIRTIME_VERSION = "2.2.0"
# TODO : Place these functions in some common module. Right now, media
# monitor uses the same functions and it would be better to reuse them
# instead of copy pasting them around
def to_unicode(obj, encoding='utf-8'): def to_unicode(obj, encoding='utf-8'):
if isinstance(obj, basestring): if isinstance(obj, basestring):
if not isinstance(obj, unicode): if not isinstance(obj, unicode):
@ -39,7 +44,7 @@ def convert_dict_value_to_utf8(md):
# Airtime API Client # Airtime API Client
################################################################################ ################################################################################
class AirtimeApiClient(): class AirtimeApiClient(object):
# This is a little hacky fix so that I don't have to pass the config object # This is a little hacky fix so that I don't have to pass the config object
# everywhere where AirtimeApiClient needs to be initialized # everywhere where AirtimeApiClient needs to be initialized
@ -422,53 +427,46 @@ class AirtimeApiClient():
def send_media_monitor_requests(self, action_list, dry=False): def send_media_monitor_requests(self, action_list, dry=False):
""" """
Send a gang of media monitor events at a time. actions_list is a list Send a gang of media monitor events at a time. actions_list is a
of dictionaries where every dictionary is representing an action. Every list of dictionaries where every dictionary is representing an
action dict must contain a 'mode' key that says what kind of action it action. Every action dict must contain a 'mode' key that says
is and an optional 'is_record' key that says whether the show was what kind of action it is and an optional 'is_record' key that
recorded or not. The value of this key does not matter, only if it's says whether the show was recorded or not. The value of this key
present or not. does not matter, only if it's present or not.
""" """
logger = self.logger url = self.construct_url('reload_metadata_group')
try: # We are assuming that action_list is a list of dictionaries such
url = self.construct_url('reload_metadata_group') # that every dictionary represents the metadata of a file along
# We are assuming that action_list is a list of dictionaries such # with a special mode key that is the action to be executed by the
# that every dictionary represents the metadata of a file along # controller.
# with a special mode key that is the action to be executed by the valid_actions = []
# controller. # We could get a list of valid_actions in a much shorter way using
valid_actions = [] # filter but here we prefer a little more verbosity to help
# We could get a list of valid_actions in a much shorter way using # debugging
# filter but here we prefer a little more verbosity to help for action in action_list:
# debugging if not 'mode' in action:
for action in action_list: self.logger.debug("Warning: Trying to send a request element without a 'mode'")
if not 'mode' in action: self.logger.debug("Here is the the request: '%s'" % str(action) )
self.logger.debug("Warning: Trying to send a request element without a 'mode'") else:
self.logger.debug("Here is the the request: '%s'" % str(action) ) # We alias the value of is_record to true or false no
else: # matter what it is based on if it's absent in the action
# We alias the value of is_record to true or false no if 'is_record' not in action:
# matter what it is based on if it's absent in the action action['is_record'] = 0
if 'is_record' not in action: valid_actions.append(action)
action['is_record'] = 0 # Note that we must prefix every key with: mdX where x is a number
valid_actions.append(action) # Is there a way to format the next line a little better? The
# Note that we must prefix every key with: mdX where x is a number # parenthesis make the code almost unreadable
# Is there a way to format the next line a little better? The md_list = dict((("md%d" % i), json.dumps(convert_dict_value_to_utf8(md))) \
# parenthesis make the code almost unreadable for i,md in enumerate(valid_actions))
md_list = dict((("md%d" % i), json.dumps(convert_dict_value_to_utf8(md))) \ # For testing we add the following "dry" parameter to tell the
for i,md in enumerate(valid_actions)) # controller not to actually do any changes
# For testing we add the following "dry" parameter to tell the if dry: md_list['dry'] = 1
# controller not to actually do any changes self.logger.info("Pumping out %d requests..." % len(valid_actions))
if dry: md_list['dry'] = 1 data = urllib.urlencode(md_list)
self.logger.info("Pumping out %d requests..." % len(valid_actions)) req = urllib2.Request(url, data)
data = urllib.urlencode(md_list) response = self.get_response_from_server(req)
req = urllib2.Request(url, data) response = json.loads(response)
response = self.get_response_from_server(req) return response
response = json.loads(response)
return response
except ValueError: raise
except Exception, e:
logger.error('Exception: %s', e)
logger.error("traceback: %s", traceback.format_exc())
raise
#returns a list of all db files for a given directory in JSON format: #returns a list of all db files for a given directory in JSON format:
#{"files":["path/to/file1", "path/to/file2"]} #{"files":["path/to/file1", "path/to/file2"]}

View File

@ -12,14 +12,14 @@ import traceback
""" """
list of supported easy tags in mutagen version 1.20 list of supported easy tags in mutagen version 1.20
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', ['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry',
'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', 'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby',
'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', 'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid',
'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', 'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort',
'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', 'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor',
'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', 'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid',
'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', 'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid',
'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', 'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid',
'arranger', 'albumsort', 'replaygain_*_peak', 'organization'] 'arranger', 'albumsort', 'replaygain_*_peak', 'organization']
""" """
@ -139,7 +139,7 @@ class AirtimeMetadata:
def get_md_from_file(self, filepath): def get_md_from_file(self, filepath):
""" """
Returns None if error retrieving metadata. Otherwise returns a dictionary Returns None if error retrieving metadata. Otherwise returns a dictionary
representing the file's metadata representing the file's metadata
""" """
self.logger.info("getting info from filepath %s", filepath) self.logger.info("getting info from filepath %s", filepath)
@ -248,9 +248,9 @@ class AirtimeMetadata:
try: try:
md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", 0) md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", 0)
md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", 0) md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", 0)
md['MDATA_KEY_DURATION'] = self.format_length(getattr(file_info.info, "length", 0.0)) md['MDATA_KEY_DURATION'] = self.format_length(getattr(file_info.info, "length", 0.0))
md['MDATA_KEY_MIME'] = "" md['MDATA_KEY_MIME'] = ""
if len(file_info.mime) > 0: if len(file_info.mime) > 0:
md['MDATA_KEY_MIME'] = file_info.mime[0] md['MDATA_KEY_MIME'] = file_info.mime[0]

View File

@ -124,7 +124,7 @@ def load_definitions():
default_title = re.sub(r'__\d+\.',u'.', default_title) default_title = re.sub(r'__\d+\.',u'.', default_title)
# format is: track_number-title-123kbps.mp3 # format is: track_number-title-123kbps.mp3
m = re.match(".+-(?P<title>.+)-(\d+kbps|unknown)$", default_title) m = re.match(".+?-(?P<title>.+)-(\d+kbps|unknown)$", default_title)
if m: new_title = m.group('title') if m: new_title = m.group('title')
else: new_title = re.sub(r'-\d+kbps$', u'', default_title) else: new_title = re.sub(r'-\d+kbps$', u'', default_title)

View File

@ -1,314 +0,0 @@
# These operators need to be updated..
# Stream data from mplayer
# @category Source / Input
# @param s data URI.
# @param ~restart restart on exit.
# @param ~restart_on_error restart on exit with error.
# @param ~buffer Duration of the pre-buffered data.
# @param ~max Maximum duration of the buffered data.
def input.mplayer(~id="input.mplayer",
~restart=true,~restart_on_error=false,
~buffer=0.2,~max=10.,s) =
input.external(id=id,restart=restart,
restart_on_error=restart_on_error,
buffer=buffer,max=max,
"mplayer -really-quiet -ao pcm:file=/dev/stdout \
-vc null -vo null #{quote(s)} 2>/dev/null")
end
# Output the stream using aplay.
# Using this turns "root.sync" to false
# since aplay will do the synchronisation
# @category Source / Output
# @param ~id Output's ID
# @param ~device Alsa pcm device name
# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop.
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
# @param ~on_start Callback executed when outputting starts.
# @param ~on_stop Callback executed when outputting stops.
# @param s Source to play
def output.aplay(~id="output.aplay",~device="default",
~fallible=false,~on_start={()},~on_stop={()},
~restart_on_crash=false,s)
def aplay_p(m) =
"aplay -D #{device}"
end
log(label=id,level=3,"Setting root.sync to false")
set("root.sync",false)
output.pipe.external(id=id,
fallible=fallible,on_start=on_start,on_stop=on_stop,
restart_on_crash=restart_on_crash,
restart_on_new_track=false,
process=aplay_p,s)
end
%ifdef output.icecast.external
# Output to icecast using the lame command line encoder.
# @category Source / Output
# @param ~id Output's ID
# @param ~start Start output threads on operator initialization.
# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed.
# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled.
# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop.
# @param ~restart_on_new_track Restart encoder upon new track.
# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds.
# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users.
# @param ~lame The lame binary
# @param ~bitrate Encoder bitrate
# @param ~swap Swap audio samples. Depends on local machine's endianess and lame's version. Test this parameter if you experience garbaged mp3 audio data. On intel 32 and 64 architectures, the parameter should be "true" for lame version >= 3.98.
# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty.
# @param ~protocol Protocol of the streaming server: 'http' for Icecast, 'icy' for Shoutcast.
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
# @param ~on_start Callback executed when outputting starts.
# @param ~on_stop Callback executed when outputting stops.
# @param s The source to output
def output.icecast.lame(
~id="output.icecast.lame",~start=true,
~restart=false,~restart_delay=3,
~host="localhost",~port=8000,
~user="source",~password="hackme",
~genre="Misc",~url="http://savonet.sf.net/",
~description="Liquidsoap Radio!",~public=true,
~dumpfile="",~mount="Use [name]",
~name="Use [mount]",~protocol="http",
~lame="lame",~bitrate=128,~swap=false,
~fallible=false,~on_start={()},~on_stop={()},
~restart_on_crash=false,~restart_on_new_track=false,
~restart_encoder_delay=3600,~headers=[],s)
samplerate = get(default=44100,"frame.samplerate")
samplerate = float_of_int(samplerate) / 1000.
channels = get(default=2,"frame.channels")
swap = if swap then "-x" else "" end
mode =
if channels == 2 then
"j" # Encoding in joint stereo..
else
"m"
end
# Metadata update is set by ICY with icecast
def lame_p(m)
"#{lame} -b #{bitrate} -r --bitwidth 16 -s #{samplerate} \
--signed -m #{mode} --nores #{swap} -t - -"
end
output.icecast.external(id=id,
process=lame_p,bitrate=bitrate,start=start,
restart=restart,restart_delay=restart_delay,
host=host,port=port,user=user,password=password,
genre=genre,url=url,description=description,
public=public,dumpfile=dumpfile,restart_encoder_delay=restart_encoder_delay,
name=name,mount=mount,protocol=protocol,
header=false,restart_on_crash=restart_on_crash,
restart_on_new_track=restart_on_new_track,headers=headers,
fallible=fallible,on_start=on_start,on_stop=on_stop,
s)
end
# Output to shoutcast using the lame encoder.
# @category Source / Output
# @param ~id Output's ID
# @param ~start Start output threads on operator initialization.
# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed.
# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled.
# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop.
# @param ~restart_on_new_track Restart encoder upon new track.
# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds.
# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users.
# @param ~lame The lame binary
# @param ~bitrate Encoder bitrate
# @param ~icy_reset Reset shoutcast source buffer upon connecting (necessary for NSV).
# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty.
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
# @param ~on_start Callback executed when outputting starts.
# @param ~on_stop Callback executed when outputting stops.
# @param s The source to output
def output.shoutcast.lame(
~id="output.shoutcast.mp3",~start=true,
~restart=false,~restart_delay=3,
~host="localhost",~port=8000,
~user="source",~password="hackme",
~genre="Misc",~url="http://savonet.sf.net/",
~description="Liquidsoap Radio!",~public=true,
~dumpfile="",~name="Use [mount]",~icy_reset=true,
~lame="lame",~aim="",~icq="",~irc="",
~fallible=false,~on_start={()},~on_stop={()},
~restart_on_crash=false,~restart_on_new_track=false,
~restart_encoder_delay=3600,~bitrate=128,s) =
icy_reset = if icy_reset then "1" else "0" end
headers = [("icy-aim",aim),("icy-irc",irc),
("icy-icq",icq),("icy-reset",icy_reset)]
output.icecast.lame(
id=id, headers=headers, lame=lame,
bitrate=bitrate, start=start,
restart=restart, restart_encoder_delay=restart_encoder_delay,
host=host, port=port, user=user, password=password,
genre=genre, url=url, description=description,
public=public, dumpfile=dumpfile,
restart_on_crash=restart_on_crash,
restart_on_new_track=restart_on_new_track,
name=name, mount="/", protocol="icy",
fallible=fallible,on_start=on_start,on_stop=on_stop,
s)
end
# Output to icecast using the flac command line encoder.
# @category Source / Output
# @param ~id Output's ID
# @param ~start Start output threads on operator initialization.
# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed.
# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled.
# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop.
# @param ~restart_on_new_track Restart encoder upon new track. If false, the resulting stream will have a single track.
# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds.
# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users.
# @param ~flac The flac binary
# @param ~quality Encoder quality (0..8)
# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty.
# @param ~protocol Protocol of the streaming server: 'http' for Icecast, 'icy' for Shoutcast.
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
# @param ~on_start Callback executed when outputting starts.
# @param ~on_stop Callback executed when outputting stops.
# @param s The source to output
def output.icecast.flac(
~id="output.icecast.flac",~start=true,
~restart=false,~restart_delay=3,
~host="localhost",~port=8000,
~user="source",~password="hackme",
~genre="Misc",~url="http://savonet.sf.net/",
~description="Liquidsoap Radio!",~public=true,
~dumpfile="",~mount="Use [name]",
~name="Use [mount]",~protocol="http",
~flac="flac",~quality=6,
~restart_on_crash=false,
~restart_on_new_track=true,
~restart_encoder_delay=(-1),
~fallible=false,~on_start={()},~on_stop={()},
s)
# We will use raw format, to
# bypass input length value in WAV
# header (input length is not known)
channels = get(default=2,"frame.channels")
samplerate = get(default=44100,"frame.samplerate")
def flac_p(m)=
def option(x) =
"-T #{quote(fst(x))}=#{quote(snd(x))}"
end
m = list.map(option,m)
m = string.concat(separator=" ",m)
"#{flac} --force-raw-format --endian=little --channels=#{channels} \
--bps=16 --sample-rate=#{samplerate} --sign=signed #{m} \
-#{quality} --ogg -c -"
end
output.icecast.external(id=id,
process=flac_p,bitrate=(-1),start=start,
restart=restart,restart_delay=restart_delay,
host=host,port=port,user=user,password=password,
genre=genre,url=url,description=description,
public=public,dumpfile=dumpfile,
name=name,mount=mount,protocol=protocol,
fallible=fallible,on_start=on_start,on_stop=on_stop,
restart_on_new_track=restart_on_new_track,
format="ogg",header=false,icy_metadata=false,
restart_on_crash=restart_on_crash,
restart_encoder_delay=restart_encoder_delay,
s)
end
# Output to icecast using the aacplusenc command line encoder.
# @category Source / Output
# @param ~id Output's ID
# @param ~start Start output threads on operator initialization.
# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed.
# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled.
# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop.
# @param ~restart_on_new_track Restart encoder upon new track.
# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds.
# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users.
# @param ~aacplusenc The aacplusenc binary
# @param ~bitrate Encoder bitrate
# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty.
# @param ~protocol Protocol of the streaming server: 'http' for Icecast, 'icy' for Shoutcast.
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
# @param ~on_start Callback executed when outputting starts.
# @param ~on_stop Callback executed when outputting stops.
# @param s The source to output
def output.icecast.aacplusenc(
~id="output.icecast.aacplusenc",~start=true,
~restart=false,~restart_delay=3,
~host="localhost",~port=8000,
~user="source",~password="hackme",
~genre="Misc",~url="http://savonet.sf.net/",
~description="Liquidsoap Radio!",~public=true,
~dumpfile="",~mount="Use [name]",
~name="Use [mount]",~protocol="http",
~aacplusenc="aacplusenc",~bitrate=64,
~fallible=false,~on_start={()},~on_stop={()},
~restart_on_crash=false,~restart_on_new_track=false,
~restart_encoder_delay=3600,~headers=[],s)
# Metadata update is set by ICY with icecast
def aacplusenc_p(m)
"#{aacplusenc} - - #{bitrate}"
end
output.icecast.external(id=id,
process=aacplusenc_p,bitrate=bitrate,start=start,
restart=restart,restart_delay=restart_delay,
host=host,port=port,user=user,password=password,
genre=genre,url=url,description=description,
public=public,dumpfile=dumpfile,
name=name,mount=mount,protocol=protocol,
fallible=fallible,on_start=on_start,on_stop=on_stop,
header=true,restart_on_crash=restart_on_crash,
restart_on_new_track=restart_on_new_track,headers=headers,
restart_encoder_delay=restart_encoder_delay,format="audio/aacp",s)
end
# Output to shoutcast using the aacplusenc encoder.
# @category Source / Output
# @param ~id Output's ID
# @param ~start Start output threads on operator initialization.
# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed.
# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled.
# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop.
# @param ~restart_on_new_track Restart encoder upon new track.
# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds.
# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users.
# @param ~aacplusenc The aacplusenc binary
# @param ~bitrate Encoder bitrate
# @param ~icy_reset Reset shoutcast source buffer upon connecting (necessary for NSV).
# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty.
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
# @param ~on_start Callback executed when outputting starts.
# @param ~on_stop Callback executed when outputting stops.
# @param s The source to output
def output.shoutcast.aacplusenc(
~id="output.shoutcast.aacplusenc",~start=true,
~restart=false,~restart_delay=3,
~host="localhost",~port=8000,
~user="source",~password="hackme",
~genre="Misc",~url="http://savonet.sf.net/",
~description="Liquidsoap Radio!",~public=true,
~fallible=false,~on_start={()},~on_stop={()},
~dumpfile="",~name="Use [mount]",~icy_reset=true,
~aim="",~icq="",~irc="",~aacplusenc="aacplusenc",
~restart_on_crash=false,~restart_on_new_track=false,
~restart_encoder_delay=3600,~bitrate=64,s) =
icy_reset = if icy_reset then "1" else "0" end
headers = [("icy-aim",aim),("icy-irc",irc),
("icy-icq",icq),("icy-reset",icy_reset)]
output.icecast.aacplusenc(
id=id, headers=headers, aacplusenc=aacplusenc,
bitrate=bitrate, start=start,
restart=restart, restart_delay=restart_delay,
host=host, port=port, user=user, password=password,
genre=genre, url=url, description=description,
public=public, dumpfile=dumpfile,
fallible=fallible,on_start=on_start,on_stop=on_stop,
restart_on_crash=restart_on_crash, restart_encoder_delay=restart_encoder_delay,
restart_on_new_track=restart_on_new_track,
name=name, mount="/", protocol="icy",
s)
end
%endif

View File

@ -4,6 +4,7 @@
# Enable external Musepack decoder. Requires the # Enable external Musepack decoder. Requires the
# mpcdec binary in the path. Does not work on # mpcdec binary in the path. Does not work on
# Win32. # Win32.
# @category Liquidsoap
def enable_external_mpc_decoder() = def enable_external_mpc_decoder() =
# A list of know extensions and content-type for Musepack. # A list of know extensions and content-type for Musepack.
# Values from http://en.wikipedia.org/wiki/Musepack # Values from http://en.wikipedia.org/wiki/Musepack

View File

@ -13,18 +13,12 @@ def http_response(~protocol="HTTP/1.1",
~headers=[], ~headers=[],
~data="") = ~data="") =
status = http_codes[string_of(code)] status = http_codes[string_of(code)]
# Set content-length if needed and not set by the # Set content-length and connection: close
# user.
headers = headers =
if data != "" and list.append(headers,
not list.mem_assoc("Content-Length",headers) [("Content-Length", "#{string.length(data)}"),
then ("Connection", "close")])
list.append([("Content-Length",
"#{string.length(data)}")],
headers)
else
headers
end
headers = list.map(fun (x) -> "#{fst(x)}: #{snd(x)}",headers) headers = list.map(fun (x) -> "#{fst(x)}: #{snd(x)}",headers)
headers = string.concat(separator="\r\n",headers) headers = string.concat(separator="\r\n",headers)
# If no headers are provided, we should avoid # If no headers are provided, we should avoid

View File

@ -4,4 +4,5 @@
%include "lastfm.liq" %include "lastfm.liq"
%include "flows.liq" %include "flows.liq"
%include "http.liq" %include "http.liq"
%include "video_text.liq" %include "video_text.liq"
%include "gstreamer.liq"

View File

@ -21,10 +21,14 @@ end
# @param a Key to look for # @param a Key to look for
# @param l List of pairs (key,value) # @param l List of pairs (key,value)
def list.mem_assoc(a,l) def list.mem_assoc(a,l)
v = list.assoc(a,l) def f(cur, el) =
# We check for existence, since "" may indicate if not cur then
# either a binding (a,"") or no binding.. fst(el) == a
list.mem((a,v),l) else
cur
end
end
list.fold(f, false, l)
end end
# Remove a pair from an associative list # Remove a pair from an associative list
@ -164,8 +168,7 @@ def out(s)
output.prefered(mksafe(s)) output.prefered(mksafe(s))
end end
# Special track insensitive fallback that # Special track insensitive fallback that always skips current song before switching.
# always skip current song before switching.
# @category Source / Track Processing # @category Source / Track Processing
# @param ~input The input source # @param ~input The input source
# @param f The fallback source # @param f The fallback source
@ -212,14 +215,17 @@ end
# Simple crossfade. # Simple crossfade.
# @category Source / Track Processing # @category Source / Track Processing
# @param ~start_next Duration in seconds of the crossed end of track. # @param ~start_next Duration in seconds of the crossed end of track.
# @param ~fade_in Duration of the fade in for next track # @param ~fade_in Duration of the fade in for next track.
# @param ~fade_out Duration of the fade out for previous track # @param ~fade_out Duration of the fade out for previous track.
# @param s The source to use # @param ~conservative Always prepare for a premature end-of-track.
def crossfade(~id="",~start_next,~fade_in,~fade_out,s) # @param s The source to use.
def crossfade(~id="",~conservative=true,
~start_next=5.,~fade_in=3.,~fade_out=3.,
s)
s = fade.in(duration=fade_in,s) s = fade.in(duration=fade_in,s)
s = fade.out(duration=fade_out,s) s = fade.out(duration=fade_out,s)
fader = fun (a,b) -> add(normalize=false,[b,a]) fader = fun (a,b) -> add(normalize=false,[b,a])
cross(id=id,conservative=true,duration=start_next,fader,s) cross(id=id,conservative=conservative,duration=start_next,fader,s)
end end
# Append speech-synthesized tracks reading the metadata. # Append speech-synthesized tracks reading the metadata.
@ -242,8 +248,7 @@ def helium(s)
end end
%endif %endif
# Return true if process exited with 0 code. # Return true if process exited with 0 code. Command should return quickly.
# Command should return quickly.
# @category System # @category System
# @param command Command to test # @param command Command to test
def test_process(command) def test_process(command)
@ -277,12 +282,9 @@ def url.split(uri) =
end end
end end
# Register a server/telnet command to # Register a server/telnet command to update a source's metadata. Returns
# update a source's metadata. Returns # a new source, which will receive the updated metadata. The command has
# a new source, which will receive the # the following format: insert key1="val1",key2="val2",...
# updated metadata. It behaves just like
# the pre-1.0 insert_metadata() operator,
# i.e. insert key1="val1",key2="val2",...
# @category Source / Track Processing # @category Source / Track Processing
# @param ~id Force the value of the source ID. # @param ~id Force the value of the source ID.
def server.insert_metadata(~id="",s) = def server.insert_metadata(~id="",s) =
@ -424,15 +426,15 @@ end
# @param ~conservative Always prepare for a premature end-of-track. # @param ~conservative Always prepare for a premature end-of-track.
# @param ~default Transition used when no rule applies \ # @param ~default Transition used when no rule applies \
# (default: sequence). # (default: sequence).
# @param ~high Value, in dB, for loud sound level # @param ~high Value, in dB, for loud sound level.
# @param ~medium Value, in dB, for medium sound level # @param ~medium Value, in dB, for medium sound level.
# @param ~margin Margin to detect sources that have too different \ # @param ~margin Margin to detect sources that have too different \
# sound level for crossing. # sound level for crossing.
# @param s The input source. # @param s The input source.
def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3., def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
~default=(fun (a,b) -> sequence([a, b])), ~default=(fun (a,b) -> sequence([a, b])),
~high=-15., ~medium=-32., ~margin=4., ~high=-15., ~medium=-32., ~margin=4.,
~width=2.,~conservative=false,s) ~width=2.,~conservative=true,s)
fade.out = fade.out(type="sin",duration=fade_out) fade.out = fade.out(type="sin",duration=fade_out)
fade.in = fade.in(type="sin",duration=fade_in) fade.in = fade.in(type="sin",duration=fade_in)
add = fun (a,b) -> add(normalize=false,[b, a]) add = fun (a,b) -> add(normalize=false,[b, a])
@ -549,7 +551,18 @@ def playlist.reloadable(~id="",~random=false,~on_done={()},uri)
if request.resolve(playlist) then if request.resolve(playlist) then
playlist = request.filename(playlist) playlist = request.filename(playlist)
files = playlist.parse(playlist) files = playlist.parse(playlist)
list.map(snd,files) def file_request(el) =
meta = fst(el)
file = snd(el)
s = list.fold(fun (cur, el) ->
"#{cur},#{fst(el)}=#{string.escape(snd(el))}", "", meta)
if s == "" then
file
else
"annotate:#{s}:#{file}"
end
end
list.map(file_request,files)
else else
log(label=id,"Couldn't read playlist: request resolution failed.") log(label=id,"Couldn't read playlist: request resolution failed.")
[] []

View File

@ -1,5 +1,6 @@
%ifdef video.add_text.gd %ifdef video.add_text.gd
# Add a scrolling line of text on video frames. # Add a scrolling line of text on video frames.
# @category Source / Video Processing
# @param ~id Force the value of the source ID. # @param ~id Force the value of the source ID.
# @param ~color Text color (in 0xRRGGBB format). # @param ~color Text color (in 0xRRGGBB format).
# @param ~cycle Cycle text. # @param ~cycle Cycle text.
@ -22,6 +23,7 @@ end
%ifdef video.add_text.sdl %ifdef video.add_text.sdl
# Add a scrolling line of text on video frames. # Add a scrolling line of text on video frames.
# @category Source / Video Processing
# @param ~id Force the value of the source ID. # @param ~id Force the value of the source ID.
# @param ~color Text color (in 0xRRGGBB format). # @param ~color Text color (in 0xRRGGBB format).
# @param ~cycle Cycle text. # @param ~cycle Cycle text.

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import traceback
""" """
Python part of radio playout (pypo) Python part of radio playout (pypo)
@ -102,6 +103,24 @@ class Notify:
logger.debug('# Calling server to update webstream data #') logger.debug('# Calling server to update webstream data #')
logger.debug('#################################################') logger.debug('#################################################')
response = self.api_client.notify_webstream_data(data, media_id) response = self.api_client.notify_webstream_data(data, media_id)
logger.debug("Response: " + json.dumps(response))
def run_with_options(self, options):
if options.error and options.stream_id:
self.notify_liquidsoap_status(options.error, options.stream_id, options.time)
elif options.connect and options.stream_id:
self.notify_liquidsoap_status("OK", options.stream_id, options.time)
elif options.source_name and options.source_status:
self.notify_source_status(options.source_name, options.source_status)
elif options.webstream:
self.notify_webstream_data(options.webstream, options.media_id)
elif options.media_id:
self.notify_media_start_playing(options.media_id)
elif options.liquidsoap_started:
self.notify_liquidsoap_started()
else:
logger.debug("Unrecognized option in options(%s). Doing nothing" \
% str(options))
if __name__ == '__main__': if __name__ == '__main__':
@ -112,41 +131,9 @@ if __name__ == '__main__':
print '#########################################' print '#########################################'
# initialize # initialize
if options.error and options.stream_id: try:
try: n = Notify()
n = Notify() n.run_with_options(options)
n.notify_liquidsoap_status(options.error, options.stream_id, options.time) except Exception as e:
except Exception, e: print( traceback.format_exc() )
print e
elif options.connect and options.stream_id:
try:
n = Notify()
n.notify_liquidsoap_status("OK", options.stream_id, options.time)
except Exception, e:
print e
elif options.source_name and options.source_status:
try:
n = Notify()
n.notify_source_status(options.source_name, options.source_status)
except Exception, e:
print e
elif options.webstream:
try:
n = Notify()
n.notify_webstream_data(options.webstream, options.media_id)
except Exception, e:
print e
elif options.media_id:
try:
n = Notify()
n.notify_media_start_playing(options.media_id)
except Exception, e:
print e
elif options.liquidsoap_started:
try:
n = Notify()
n.notify_liquidsoap_started()
except Exception, e:
print e

View File

@ -21,7 +21,7 @@ logger.addHandler(ch)
if (os.geteuid() != 0): if (os.geteuid() != 0):
print 'Must be a root user.' print 'Must be a root user.'
sys.exit() sys.exit()
# loading config file # loading config file
try: try:
config = ConfigObj('/etc/airtime/media-monitor.cfg') config = ConfigObj('/etc/airtime/media-monitor.cfg')
@ -66,12 +66,12 @@ def copy_or_move_files_to(paths, dest, flag):
print "Cannot find file or path: %s" % path print "Cannot find file or path: %s" % path
except Exception as e: except Exception as e:
print "Error: ", e print "Error: ", e
def format_dir_string(path): def format_dir_string(path):
if(path[-1] != '/'): if(path[-1] != '/'):
path = path+'/' path = path+'/'
return path return path
def helper_get_stor_dir(): def helper_get_stor_dir():
res = api_client.list_all_watched_dirs() res = api_client.list_all_watched_dirs()
if(res is None): if(res is None):
@ -87,18 +87,18 @@ def checkOtherOption(args):
for i in args: for i in args:
if(i[0] == '-'): if(i[0] == '-'):
return True return True
def errorIfMultipleOption(args, msg=''): def errorIfMultipleOption(args, msg=''):
if(checkOtherOption(args)): if(checkOtherOption(args)):
if(msg != ''): if(msg != ''):
raise OptionValueError(msg) raise OptionValueError(msg)
else: else:
raise OptionValueError("This option cannot be combined with other options") raise OptionValueError("This option cannot be combined with other options")
def printHelp(): def printHelp():
storage_dir = helper_get_stor_dir() storage_dir = helper_get_stor_dir()
if(storage_dir is None): if(storage_dir is None):
storage_dir = "Unknown" storage_dir = "Unknown"
else: else:
storage_dir += "imported/" storage_dir += "imported/"
print """ print """
@ -111,15 +111,15 @@ There are two ways to import audio files into Airtime:
Copied or moved files will be placed into the folder: Copied or moved files will be placed into the folder:
%s %s
Files will be automatically organized into the structure Files will be automatically organized into the structure
"Artist/Album/TrackNumber-TrackName-Bitrate.file_extension". "Artist/Album/TrackNumber-TrackName-Bitrate.file_extension".
2) Use airtime-import to add a folder to the Airtime library ("watch" a folder). 2) Use airtime-import to add a folder to the Airtime library ("watch" a folder).
All the files in the watched folder will be imported to Airtime and the All the files in the watched folder will be imported to Airtime and the
folder will be monitored to automatically detect any changes. Hence any folder will be monitored to automatically detect any changes. Hence any
changes done in the folder(add, delete, edit a file) will trigger changes done in the folder(add, delete, edit a file) will trigger
updates in Airtime library. updates in Airtime library.
""" % storage_dir """ % storage_dir
parser.print_help() parser.print_help()
@ -209,7 +209,7 @@ def WatchRemoveAction(option, opt, value, parser):
print "Removing the watch folder failed: %s" % res['msg']['error'] print "Removing the watch folder failed: %s" % res['msg']['error']
else: else:
print "The given path is not a directory: %s" % path print "The given path is not a directory: %s" % path
def StorageSetAction(option, opt, value, parser): def StorageSetAction(option, opt, value, parser):
bypass = False bypass = False
isF = '-f' in parser.rargs isF = '-f' in parser.rargs
@ -231,12 +231,12 @@ def StorageSetAction(option, opt, value, parser):
confirm = confirm or 'N' confirm = confirm or 'N'
if(confirm == 'n' or confirm =='N'): if(confirm == 'n' or confirm =='N'):
sys.exit(1) sys.exit(1)
if(len(parser.rargs) > 1): if(len(parser.rargs) > 1):
raise OptionValueError("Too many arguments. This option requires exactly one argument.") raise OptionValueError("Too many arguments. This option requires exactly one argument.")
elif(len(parser.rargs) == 0 ): elif(len(parser.rargs) == 0 ):
raise OptionValueError("No argument found. This option requires exactly one argument.") raise OptionValueError("No argument found. This option requires exactly one argument.")
path = parser.rargs[0] path = parser.rargs[0]
if (path[0] == "/" or path[0] == "~"): if (path[0] == "/" or path[0] == "~"):
path = os.path.realpath(path) path = os.path.realpath(path)
@ -254,17 +254,17 @@ def StorageSetAction(option, opt, value, parser):
print "Setting storage folder failed: %s" % res['msg']['error'] print "Setting storage folder failed: %s" % res['msg']['error']
else: else:
print "The given path is not a directory: %s" % path print "The given path is not a directory: %s" % path
def StorageGetAction(option, opt, value, parser): def StorageGetAction(option, opt, value, parser):
errorIfMultipleOption(parser.rargs) errorIfMultipleOption(parser.rargs)
if(len(parser.rargs) > 0): if(len(parser.rargs) > 0):
raise OptionValueError("This option does not take any arguments.") raise OptionValueError("This option does not take any arguments.")
print helper_get_stor_dir() print helper_get_stor_dir()
class OptionValueError(RuntimeError): class OptionValueError(RuntimeError):
def __init__(self, msg): def __init__(self, msg):
self.msg = msg self.msg = msg
usage = """[-c|--copy FILE/DIR [FILE/DIR...]] [-m|--move FILE/DIR [FILE/DIR...]] usage = """[-c|--copy FILE/DIR [FILE/DIR...]] [-m|--move FILE/DIR [FILE/DIR...]]
[--watch-add DIR] [--watch-list] [--watch-remove DIR] [--watch-add DIR] [--watch-list] [--watch-remove DIR]
[--storage-dir-set DIR] [--storage-dir-get]""" [--storage-dir-set DIR] [--storage-dir-get]"""
@ -293,7 +293,7 @@ if('-h' in sys.argv):
if(len(sys.argv) == 1 or '-' not in sys.argv[1]): if(len(sys.argv) == 1 or '-' not in sys.argv[1]):
printHelp() printHelp()
sys.exit() sys.exit()
try: try:
(option, args) = parser.parse_args() (option, args) = parser.parse_args()
except Exception, e: except Exception, e:
@ -306,7 +306,7 @@ except Exception, e:
except SystemExit: except SystemExit:
printHelp() printHelp()
sys.exit() sys.exit()
if option.help: if option.help:
printHelp() printHelp()
sys.exit() sys.exit()

View File

@ -1,57 +1,57 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Airtime widgets</title> <title>Airtime widgets</title>
<script src="js/jquery-1.6.1.min.js" type="text/javascript"></script> <script src="js/jquery-1.6.1.min.js" type="text/javascript"></script>
<script src="js/jquery-ui-1.8.10.custom.min.js" type="text/javascript"></script> <script src="js/jquery-ui-1.8.10.custom.min.js" type="text/javascript"></script>
<script src="js/jquery.showinfo.js" type="text/javascript"></script> <script src="js/jquery.showinfo.js" type="text/javascript"></script>
<link href="css/airtime-widgets.css" rel="stylesheet" type="text/css" /> <link href="css/airtime-widgets.css" rel="stylesheet" type="text/css" />
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$("#headerLiveHolder").airtimeLiveInfo({ $("#headerLiveHolder").airtimeLiveInfo({
sourceDomain: "http://localhost", sourceDomain: "http://localhost",
text: {onAirNow:"On Air Now", offline:"Offline", current:"Current", next:"Next"}, text: {onAirNow:"On Air Now", offline:"Offline", current:"Current", next:"Next"},
updatePeriod: 20 //seconds updatePeriod: 20 //seconds
}); });
$("#onAirToday").airtimeShowSchedule({ $("#onAirToday").airtimeShowSchedule({
sourceDomain: "http://localhost", sourceDomain: "http://localhost",
text: {onAirToday:"On air today"}, text: {onAirToday:"On air today"},
updatePeriod: 5, //seconds updatePeriod: 5, //seconds
showLimit: 10 showLimit: 10
}); });
$("#scheduleTabs").airtimeWeekSchedule({ $("#scheduleTabs").airtimeWeekSchedule({
sourceDomain:"http://localhost", sourceDomain:"http://localhost",
dowText:{monday:"Monday", tuesday:"Tuesday", wednesday:"Wednesday", thursday:"Thursday", friday:"Friday", saturday:"Saturday", sunday:"Sunday"}, dowText:{monday:"Monday", tuesday:"Tuesday", wednesday:"Wednesday", thursday:"Thursday", friday:"Friday", saturday:"Saturday", sunday:"Sunday"},
miscText:{time:"Time", programName:"Program Name", details:"Details", readMore:"Read More"}, miscText:{time:"Time", programName:"Program Name", details:"Details", readMore:"Read More"},
updatePeriod: 600 //seconds updatePeriod: 600 //seconds
}); });
var d = new Date().getDay(); var d = new Date().getDay();
$('#scheduleTabs').tabs({selected: d === 0 ? 6 : d-1, fx: { opacity: 'toggle' }}); $('#scheduleTabs').tabs({selected: d === 0 ? 6 : d-1, fx: { opacity: 'toggle' }});
}); });
</script> </script>
</head> </head>
<body> <body>
<h1>"Now Playing" Widget</h1> <h1>"Now Playing" Widget</h1>
This widget displays what is currently playing: This widget displays what is currently playing:
<br/> <br/>
<br/> <br/>
<div id="headerLiveHolder" style="border: 1px solid #999999; padding: 10px;"></div> <div id="headerLiveHolder" style="border: 1px solid #999999; padding: 10px;"></div>
<br/> <br/>
<br/> <br/>
<h1>"Today's Program" Widget</h1> <h1>"Today's Program" Widget</h1>
This widget displays what is being played today: This widget displays what is being played today:
<br/> <br/>
<br/> <br/>
<div id="onAirToday"></div> <div id="onAirToday"></div>
<br/> <br/>
<br/> <br/>
<h1>"Weekly Program" Widget</h1> <h1>"Weekly Program" Widget</h1>
This widget displays all the shows for the entire week: This widget displays all the shows for the entire week:
<br/> <br/>
<br/> <br/>
<div id="scheduleTabs"></div> <div id="scheduleTabs"></div>
</body> </body>
</html> </html>