Difference between revisions of "Shoutcast - radio controller v0.3 (remake of similar scripts)"
m (→TODO) |
|||
Line 945: | Line 945: | ||
[GENRE] | [GENRE] | ||
# genre name | # genre name -- Use short genre names as the the length of menu buttons is short! | ||
Hard Rock | Hard Rock | ||
Classic Rock | Classic Rock |
Revision as of 17:25, 9 February 2011
LSL Portal | Functions | Events | Types | Operators | Constants | Flow Control | Script Library | Categorized Library | Tutorials |
Shoutcast - radio controller
This script controls a shoutcast radio receiver and lets you select the stations that you configured on a notecard. Menu driven. Output of station description, genre and current song title using Xy text.
Versions
Current version: v0.3 released 10-2-2011
Main script
This script should be put into the <lsl> // Script: Shoutcast - radio controller // Version: 0.3 - released 10-2-2011 // Logic Scripts (Flennan Roffo) // (c) 2010 - Flennan Roffo (Logic Scripts) // // This script is a remake of a couple of similar script: // + LandOwnersRadio V2.0 by Scripter Coba (( menu driven / notecard config script to select the radio station and sets parcel music url )) // + Raven radio infoboard by Jamie Otis (( worked at the basis of sis service [sis.slserver.com/sis.php] used Xy text display )) // + currentPlaying by Darkie Minotaur (( used the /7.html info to fetch current song title info, displayed as float text )) // // Purpose: // * Sets the parcel audio URL and displays the channel info // * Uses Xytext to display the info. // * Fetches song title info from the shoutcast url /////////////////////////////////////////////////////////////////////////////////////// // Extra Features -- 0.1 release // * On/Off option // * Allows multiple menus (if options per menu > 12) using a prev/next button // * Checks if your url is well-formatted // * Will delete genres for which no stations exist // * Will skip stations that have same url and same genre (you can however have an identical station url under different genres). // * New notecard format ////////////////////////////////////////////////////////////////////////////////////////// // Extra Features -- 0.2 release // * Configurable button text // * Gets parcel URL and automatically sets the genre/station and on/off status accordingly (<-- doesn't work) ////////////////////////////////////////////////////////////////////////////////////////// // Update -- 0.3 release // * Fixed bug (only first station in genre displayed in menu) // * Auto reset script when config card updated // Notes: // * Expects url to be in the format: <ip>:<port>, where <ip> has the format: xxx.xxx.xxx.xxx // * Deletes entries in category (genre) for which no stations are configured with notice. // * Skips stations which have identical URL AND same category (genre). ////////////////////////////////////////////////////////////////////////////////////////// // Upcoming release -- 0.4 // * Will add functions for remote controller(s) and remote display(s) using llRegionSay to communicate over a channel. // * Should relax on the constraints about the input format of URL's (currently requires that URL has format: xxx.xxx.xxx.xxx/yyyy) // * Fix button placement // * Implement script reset on change of owner // * Permit station to be put under multiple genres, using a comma-seperated list of genres in the section [STATION] // (currently this is only possible by duplicating the entire line and change the genre.) ///////////////////////////////////////////////////////////////////////////////////////// // BUGS & FEATURE REQUESTS // // Please inform the author, Logic Scripts (flennan.roffo) about any bugs or annoyances. // Feature requests can also be submitted to the author. ///////////////////////////////////////////////////////////////////////////////////////// // LICENCE INFO // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. ///////////////////////////////////////////////////////////////////////////////////////////
string info_notecard="Radio Control info"; /////// EDITABLE \\\\\\ string config_notecard="Radio Control config"; /////// EDITABLE \\\\\\ string comment_char="#"; /////// EDITABLE \\\\\\ list sep_char_list= ["|"]; /////// EDITABLE \\\\\\ float update_time=5.0; /////// EDITABLE \\\\\\ string no_title_info="(no title info available)"; /////// EDITABLE \\\\\\
// not used currently - for showing info on current song title elsewhere in the region integer broadcast_channel=-1234; /////// EDITABLE \\\\\\
// Buttons
string button_MAIN = "MAIN"; string button_HELP = "HELP"; string button_NEXT = ">>"; string button_PREV = "<<"; string button_ON = "ON"; string button_OFF = "OFF";
// List of categories (=genres)
list category_list=[];
// List of stations. KEEP THESE LISTS IN SYNCH!
list station_category=[]; list station_name=[]; list station_desc=[]; list station_url=[];
// Last song title played string last_title_info="";
integer radio_status=0; // 0 - OFF 1 - ON string parcel_url=""; integer lineno=0; key reqid=NULL_KEY; key httpreq_id=NULL_KEY; integer config_error=FALSE; integer flag; integer section=0;
// Access values. Note that users who are banned can not access the device even when access is public integer owner_access=TRUE; integer group_access=FALSE; integer public_access=TRUE; list banned_keys=[];
// Channels for menu and user input integer menu_channel; integer listen_handle;
// Menu integer menu_type=0; // 0 - Main menu (genres) 1 - Station menu (stations) integer menu_num=0; // When more menu options need to be selectable then can be displayed on a menu (12), this is the menu number - menu number 0 is the first menu.
// Genres and stations
integer category_index=0; // Current index in category_list (genre) integer station_index=0; // Current index in station_* (station)
integer num_categories=0; integer num_stations=0;
// Make request for title info using HTTP request retrieve_titelinfo() {
string url=llList2String(station_url,station_index); httpreq_id=llHTTPRequest(url + "/7.html HTTP/1.0\nUser-Agent: LSL Script (Mozilla Compatible)\n\n",[],"");
}
// Display a line on an Xytext device linked in display_line(string line, string message) {
// Setup XYtext Variables integer DISPLAY_STRING = 204000;
// integer DISPLAY_EXTENDED = 204001; (not used) // integer REMAP_INDICES = 204002; (not used) // integer RESET_INDICES = 204003; (not used) // integer SET_CELL_INFO = 204004; (not used) // integer SET_FONT_TEXTURE = 204005; (not used) // integer SET_THICKNESS = 204006; (not used) // integer SET_COLOR = 204007; (not used)
llMessageLinked(LINK_SET,DISPLAY_STRING,message,line);
}
// Clear the Xytext display clear_display() {
// Clears the display display_line("1","Radio Station ID"); display_line("2","Music Genre...."); display_line("3","Now Playing....");
}
// Make a menu / dialog make_menu(key id) {
menu_channel=random_channel();
if (radio_status == 0) { menu_type=0; menu_num=0; llDialog(id,"Menu: Status\n\nRadio is OFF", [ "ON", "HELP" ],menu_channel); } else { if (menu_type ==0) { llDialog(id,"Menu: Genres", category_menu(menu_num),menu_channel); } else { llDialog(id,"Menu: Stations\nGenre: " + llList2String(category_list,category_index), station_menu(menu_num),menu_channel); } }
if (listen_handle != 0) llListenRemove(listen_handle); listen_handle=llListen(menu_channel,"",id,"");
}
// Make the menu option list for menu: catagories (genres) list category_menu(integer num) {
integer len=llGetListLength(category_list); list menu=[];
if (len > 9) // If more then 9 items (12 minus the 3 buttons for MAIN/HELP and PREV, NEXT) { integer last_sub=(len-1)/9; // submenus start at 0. 9th entry is in submenu 0, 10th in 1, etc.
if (num > last_sub) { llWhisper(0,"error: wrong submenu number: " + (string) num + "."); return [ "MAIN" ]; } else { integer first=9*num;
while (--len >= first) menu+=(list)llList2String(category_list,len);
if (num == 0) menu+=(list)button_HELP; else menu+=(list)button_MAIN;
if (num == 0) menu+=(list)button_OFF; else menu+=(list)button_PREV;
if (num != last_sub) menu+=(list)button_NEXT; } } else { while (--len >= 0) menu+=(list)llList2String(category_list,len);
menu+=(list)button_OFF; menu+=(list)button_HELP; } return menu; // order_buttons(menu);
}
// Returns the number of stations in a certain category integer stations_in_category(integer cat) {
integer count=0; integer i; integer len=llGetListLength(station_category); string category=llList2String(category_list,cat);
for (i=0; i < len; i++) if (category == llList2String(category_list,i)) count++;
return count;
}
// Not used currently -- to fix button placement list order_buttons(list buttons) {
integer offset; list fixt; integer flag=0;
while((offset = llGetListLength(buttons))) { if (offset > 3) flag=1; else flag=0;
fixt += llList2List(buttons, offset = -3 * flag, -1); buttons = llDeleteSubList(buttons, offset, -1); }
return fixt;
}
// Returns a list of station names in a certain category (genre) list station_list(integer category) {
list s=[]; integer i; string cname=llList2String(category_list,category_index);
for (i = 0; i < llGetListLength(station_name); i++) if (llList2String(station_category,i) == cname) s+=(list)llList2String(station_name,i);
return s;
}
// Returns the list of stations for the station menu, depending on the submenu number list station_menu(integer num) {
list stations=station_list(category_index); integer len=llGetListLength(stations); list menu=[]; if (len > 11) // 12 - 1 for MAIN menu { integer last_sub=(len-1)/9;
if (num >= last_sub) { llWhisper(0,"error: wrong submenu number: " + (string) num + "."); return [ "MAIN" ]; } else { integer first=9*num; integer last=9*num+8;
menu+=(list)button_MAIN;
if (num > 0) menu+=(list)button_PREV;
if (num < last_sub) menu+=(list)button_NEXT;
if (len > last) len =last; while (--len >= first) menu+=(list)llList2String(stations,len); } } else { menu+=(list)button_MAIN;
while (--len >= 0) menu+=(list)llList2String(stations,len); }
return menu; // order_buttons(menu);
}
// Returns whether av with key id has access integer has_access(key id) {
if (llListFindList(banned_keys,(list)id) != -1) return FALSE;
if (owner_access && id == llGetOwner()) return TRUE;
if (group_access && llSameGroup(id)) return TRUE;
if (public_access) return TRUE;
return FALSE;
}
// Gets a random channel -- uses a wide range of big negative channel numbers seldomly used integer random_channel() {
integer min=-2147483647; integer max=-1000;
return (integer) (min + llFrand(max-min));
}
// Check for the format of the url string -- is very selective about url format // expects: xxx.xxx.xxx.xxx:xxxx (ip adress in number notation with port adress) // Next release will relax on this constraint. integer check_url(string url) {
integer pos=0;
if (llGetSubString(url,0,6) == "http://") pos=7; if (llGetSubString(url,0,7) == "https://") pos=8;
if (pos==0) return FALSE;
string str_ip_port=llGetSubString(url,pos,-1); list list_ip_port=llParseString2List(str_ip_port,[":"],[]); // split in ip-adress and port list list_ip=llParseString2List(llList2String(list_ip_port,0),["."],[]); // split ip-adress elements
if (llGetListLength(list_ip_port) != 2 || llGetListLength(list_ip) != 4) return FALSE;
integer i; integer test;
for (i=0;i<4;i++) { test=llList2Integer(list_ip,i); if (llList2String(list_ip,i) != (string)test) return FALSE; if (test < 0 || test > 255) return FALSE; }
test=llList2Integer(list_ip_port,1); if (llList2String(list_ip_port,1) != (string)test) return FALSE;
return TRUE;
}
// Returns a true value depending on the first character in input - anything else is assumed false. integer true_value(string input) {
string value=llToLower(llGetSubString(input,0,0));
if (value == "y" || value == "t" || value =="1") return TRUE;
return FALSE;
}
// Return if more input should be processed (if not at EOF) - sets ConfigError if any config error found. integer process_line(string dataline) {
string line=llStringTrim(dataline,STRING_TRIM); integer index=llSubStringIndex(line,comment_char); if (index==0) // line starts with comment - ignore line return TRUE;
if (index!=-1) line=llStringTrim(llGetSubString(line,0,index-1),STRING_TRIM_TAIL); // skip everything after comment_char and trim tail
if (line=="") // Ignore blank lines return TRUE;
if (llToLower(line) == "[access]") { section = 1; return TRUE; } else if (llToLower(line) == "[banned]") { section = 2; return TRUE; } else if (llToLower(line) == "[genre]") { section = 3; return TRUE; } else if (llToLower(line) == "[station]") { section = 4; return TRUE; } else if (llGetSubString(line,0,0) == "[" && llGetSubString(line,-1,-1) == "]") { llWhisper(0,"error: malformed section found at line " + (string)lineno + ".\n" + dataline); config_error=TRUE; return FALSE; }
if (section == 0) { llWhisper(0,"error: no section found on line: " + (string) lineno); config_error = TRUE; return FALSE; }
list breakup=llParseString2List(line,["="],[]); string field=llStringTrim(llList2String(breakup,0),STRING_TRIM); string values=llStringTrim(llList2String(breakup,1),STRING_TRIM);
if (section == 1) // access { field=llToLower(field);
if (field=="owner") { owner_access=true_value(values); return TRUE; } else if (field=="group") { group_access=true_value(values); return TRUE; } else if (field=="public") { public_access=true_value(values); return TRUE; } else { llWhisper(0,"error: invalid option on line: " + (string)lineno + ".\n" + dataline); config_error=TRUE; return FALSE; }
} else if (section == 2) // ban list { key try=(key) field;
if (try) { banned_keys+=(list)((key) field); return TRUE; } else return FALSE; } else if (section == 3) // categories { if (llListFindList(category_list,(list)field) == -1) { category_list+=(list)field; } else llWhisper(0,"genre: '" + field + "' already entered; double entry skipped.");
return TRUE; } else if (section == 4) // stations { list parse=llParseString2List(line,sep_char_list, []); string category=llStringTrim(llList2String(parse,0),STRING_TRIM); string name=llStringTrim(llList2String(parse,1),STRING_TRIM); string desc=llStringTrim(llList2String(parse,2),STRING_TRIM); string url=llStringTrim(llToLower(llList2String(parse,3)),STRING_TRIM); if (!available_category(category)) { llWhisper(0,"error: unknown genre on line: " + (string)lineno + ".\n" + dataline); config_error=TRUE; return FALSE; } if (check_url(url)) { if (llListFindList(station_url,(list)url) == -1 || llListFindList(station_category,(list)category) == -1) { num_stations++; station_category+=(list)category; station_name+=(list)name; station_desc+=(list)desc; station_url+=(list)url; return TRUE; } else { llWhisper(0,"This station is already entered under the same genre and same url and is skipped.\nStation: " + name + "\nGenre: '" + category + "'\nURL: " + url); return TRUE; } } else { llWhisper(0,"error: malformed url on line: " + (string)lineno + ".\n" + dataline); config_error=TRUE; return FALSE; } } return FALSE;
}
// Sets the parcel URL and updates the display set_parcel_url(string url) {
parcel_url=url; llSetParcelMusicURL(parcel_url);
if (parcel_url=="") { clear_display(); display_line("1","Radio is OFF"); display_line("2",""); display_line("3",""); } else { llWhisper(0,"station now set to " + llList2String(station_desc,station_index) + "."); display_line("1","Station: " + llList2String(station_desc,station_index)); display_line("2","Genre : " + llList2String(category_list,category_index)); display_line("3","Now playing....."); llSetTimerEvent(update_time); }
}
// Returns if a category (genre) exists. integer available_category(string category) {
integer i; integer len=llGetListLength(category_list);
for (i=0;i<len;i++) if (llToLower(category) == llToLower(llList2String(category_list,i))) return TRUE;
return FALSE;
}
// Returns if a category (genre) is empty (NOTE: same function as available_category(), but logical negate of it. One of these can be removed.) integer empty_category(string category) {
integer i; integer len=llGetListLength(station_category);
for (i=0; i < len; i++) if (llToLower(category) == llToLower(llList2String(station_category,i))) return FALSE;
return TRUE;
}
// Removes categories (genres) for which no station is known. skip_empty_categories() {
integer i=0;
while (i<llGetListLength(category_list)) { if (empty_category(llList2String(category_list,i))) { llWhisper(0,"Warning: Genre '" + llList2String(category_list,i) + "' contains no stations and is deleted."); category_list=llDeleteSubList(category_list,i,i); } else i++; } num_categories=llGetListLength(category_list);
}
///////////////////////////////////////////// // state default ////////////////////////////////////////////
default {
state_entry() { flag=FALSE; lineno=0; config_error=FALSE; num_stations=0; num_categories=0; radio_status=0; menu_num=0; menu_type=0; if (llGetInventoryType(config_notecard) == INVENTORY_NOTECARD) { reqid=llGetNotecardLine(config_notecard,lineno++); llWhisper(0, "Reading config notecard..."); display_line("1","Reading configuration."); display_line("2","Wait...."); display_line("3",""); } else { llWhisper(0,"No config notecard '" + config_notecard + "' present."); state offline; } }
on_rez(integer param) { llResetScript(); }
dataserver(key id, string data) { if (reqid==id) { if (data==EOF) { skip_empty_categories(); llWhisper(0,"Configuration ok.\n" + (string)num_categories + " genres and " + (string)num_stations + " stations."); display_line("1","Configuration OK"); display_line("2","Genres : " + (string)num_categories); display_line("3","Stations: " + (string)num_stations); state menu; } else { if (process_line(data)) reqid=llGetNotecardLine(config_notecard,lineno++); else if (config_error) { llWhisper(0,"errors found in configuration. please correct them."); state offline; } } } }
touch_start(integer total_num) { }
changed(integer ch) { if (ch & CHANGED_INVENTORY) llResetScript(); }
}
///////////////////////////////////////////// // state offline ////////////////////////////////////////////
state offline {
state_entry() { llWhisper(0,"Reset on owner touch or when notecard updated."); } touch_start(integer t) { if (llDetectedKey(0)==llGetOwner()) llResetScript(); }
changed(integer ch) { if (ch & CHANGED_INVENTORY && flag) llResetScript(); }
}
///////////////////////////////////////////// // state menu ////////////////////////////////////////////
state menu {
state_entry() { menu_type=0; menu_num=0; listen_handle=0; }
changed(integer param) { if (param & CHANGED_INVENTORY) llResetScript(); } on_rez(integer param) { llResetScript(); }
touch_start(integer total_num) { key toucher=llDetectedKey(0);
if (has_access(toucher)) { make_menu(toucher); } else llWhisper(0,"sorry, no access."); }
listen(integer chan, string name,key id,string msg) { integer index;
if (menu_type == 0) // main menu { if (msg == button_MAIN) { menu_type=0; menu_num =0; make_menu(id); } else if (msg == button_NEXT) { menu_num++; make_menu(id); } else if (msg == button_PREV) { menu_num--; make_menu(id); } else if (msg == button_ON) { radio_status=1; set_parcel_url(parcel_url); display_line("1","Radio is ON"); menu_num=0; llWhisper(0,"Radio now turned on."); make_menu(id); } else if (msg == button_OFF) { radio_status=0; set_parcel_url(""); llSetTimerEvent(0.0); llWhisper(0,"Radio now turned off."); } else if (msg == button_HELP) { if (llGetInventoryType(info_notecard) == INVENTORY_NOTECARD) { llGiveInventory(id,info_notecard); } else llWhisper(0,"sorry, help not available."); } else if (radio_status == 1) { index = llListFindList(category_list, (list)msg);
if (index == -1) llWhisper(0,"error: genre not found: " + msg); else { category_index=index; llWhisper(0,"Genre now set to " + llList2String(category_list,category_index) + "."); menu_type=1; menu_num=0; make_menu(id); } } } else if (menu_type == 1 && radio_status == 1) // station menu { if (msg == button_MAIN) { menu_type=0; menu_num =0; make_menu(id); } else if (msg == button_NEXT) { menu_num++; make_menu(id); } else if (msg == button_PREV) { menu_num--; make_menu(id); } else { index = llListFindList(station_name, (list)msg);
if (index == -1) llWhisper(0,"error: station not found: " + msg); else { station_index=index; string new_url=llList2String(station_url,station_index);
if (new_url != parcel_url) { set_parcel_url(new_url); } } } } }
timer() { retrieve_titelinfo(); llSetTimerEvent(update_time); }
http_response(key id, integer status, list meta, string body) { if (id == httpreq_id) { if (status == 200) { string feed = llGetSubString(body,llSubStringIndex(body, "<body>") + llStringLength("<body>"), llSubStringIndex(body,"</body>") - 1); list feed_list = llParseString2List(feed,[","],[]); string current_title_info= llList2String(feed_list,6); integer length = llGetListLength(feed_list); if(llList2String(feed_list,7)) { integer a = 7; for(; a<length; ++a) { current_title_info += ", " + llList2String(feed_list,a); } } if (current_title_info != last_title_info) { last_title_info = current_title_info; display_line("3","Title : " + current_title_info); } } else { display_line("3","Title : " + no_title_info); } } }
changed(integer ch) { if (ch & CHANGED_INVENTORY && flag) llResetScript(); }
}
////////////////////////////// // end of script ////////////////////////////// </lsl>
Notecard format
Example notecard
# New format Shoutcast - radio controller config notecard # The format is divided in 4 sections: # 1 - Access configuration # 2 - Ban list (keys) # 3 - Categories (same as genres) # 4 - Station info (category, name, desc, url) # The '#' character defines the start of a comment and can be anywhere on a line. # Everything after the '#' incuding the '#' itself is ignored. # Case is not important (but information on display will show as entered) # Spaces before and/or after fields are trimmed. # Empty lines are ignored. [ACCESS] owner=yes # yes, true,1 means owner has access. everything else not. group=yes # ,, ,, ,, means group has acess. ,, ,, ,, public=yes # ,, ,, ,, means public has access. ,, ,, ,, [BANNED] # keys of banned residents [GENRE] # genre name -- Use short genre names as the the length of menu buttons is short! Hard Rock Classic Rock Oldies Rap/Urban Comedy News Dance Country Gothic # etc. [STATION] # Shoutcast radio stations # Use a '|' between each field - don't mind spaces before and after fields, they will be trimmed # field1:genre, field2:station name, field3: station description, field4: station http://<ip>:<port> or https://<ip>:<port> # Note that genre must match case-insensitive a value previously entered in section [GENRE] Classic Rock | Absolute | Absolute Radio | http://205.188.215.226:8018 # etc.
TODO
Planned for upcoming release 0.4:
- Add a llRegionSay to broadcast the <station>|<genre>|<current songtitle>|<url> to the region and add a script that displays that info on a remote Xytext board. (for use with a remote controller and/or remote display board anywhere in the region/parcel)
- llResetScript on owner change
- Relax on URL format
- Allow multiple genres per station