Difference between revisions of "Shoutcast - radio controller v0.3 (remake of similar scripts)"

From Second Life Wiki
Jump to navigation Jump to search
Line 1: Line 1:
{{LSL Header}}
{{LSL Header}}
===Shoutcast - radio controller v0.3===
=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.
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.
===Main script===
 
==Versions==
 
Current version: v0.3 released 10-2-2011
 
==Main script==
 
This script should be put into the
<lsl>
<lsl>
// Shoutcast - radio controller v0.3
// Script:  Shoutcast - radio controller
// Version: 0.3 - released 10-2-2011
// Logic Scripts (Flennan Roffo)
// Logic Scripts (Flennan Roffo)
// (c) 2010 - Flennan Roffo (Logic Scripts)
// (c) 2010 - Flennan Roffo (Logic Scripts)
//  
//  
// Remake of similar script:
// 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 ))
// + 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        ))
// + Raven radio infoboard by Jamie Otis    (( worked at the basis of sis service [sis.slserver.com/sis.php] used Xy text display        ))
Line 17: Line 25:
// * Uses Xytext to display the info.
// * Uses Xytext to display the info.
// * Fetches song title info from the shoutcast url
// * Fetches song title info from the shoutcast url
///////////////////////////////////////////////////////////////////////////////////////
// Extra Features -- 0.1 release
// Extra Features -- 0.1 release
// * On/Off option
// * On/Off option
Line 32: Line 41:
// * Fixed bug (only first station in genre displayed in menu)
// * Fixed bug (only first station in genre displayed in menu)
// * Auto reset script when config card updated
// * 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
// This program is free software: you can redistribute it and/or modify
Line 67: Line 94:
string button_OFF  = "OFF";
string button_OFF  = "OFF";


// Lists
// List of categories (=genres)


list category_list=[];
list category_list=[];
// List of stations. KEEP THESE LISTS IN SYNCH!


list station_category=[];
list station_category=[];
Line 76: Line 105:
list station_url=[];
list station_url=[];


// Last song title played
string last_title_info="";
string last_title_info="";


integer radio_status=0;
integer radio_status=0;   // 0 - OFF  1 - ON
string parcel_url="";
string parcel_url="";
integer lineno=0;
integer lineno=0;
Line 86: Line 116:
integer flag;
integer flag;
integer section=0;
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 owner_access=TRUE;
integer group_access=FALSE;
integer group_access=FALSE;
Line 91: Line 123:
list banned_keys=[];
list banned_keys=[];


// Channels for menu and user input
integer menu_channel;
integer menu_channel;
integer listen_handle;
integer listen_handle;
integer menu_type=0;
 
integer menu_num=0;
// Menu
integer category_index=0;
integer menu_type=0;         // 0 - Main menu (genres)  1 - Station menu (stations)
integer station_index=0;
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_categories=0;
integer num_stations=0;
integer num_stations=0;


// Make request for title info using HTTP request
retrieve_titelinfo()
retrieve_titelinfo()
{
{
Line 107: Line 146:
}
}


// Display a line on an Xytext device linked in
display_line(string line, string message)
display_line(string line, string message)
{
{
     // Setup XYtext Variables
     // Setup XYtext Variables
     integer DISPLAY_STRING      = 204000;  
     integer DISPLAY_STRING      = 204000;  
    integer DISPLAY_EXTENDED    = 204001;  
//    integer DISPLAY_EXTENDED    = 204001;     (not used)
    integer REMAP_INDICES      = 204002;  
//    integer REMAP_INDICES      = 204002;     (not used)
    integer RESET_INDICES      = 204003;  
//    integer RESET_INDICES      = 204003;     (not used)
    integer SET_CELL_INFO      = 204004;  
//    integer SET_CELL_INFO      = 204004;     (not used)
    integer SET_FONT_TEXTURE    = 204005;  
//    integer SET_FONT_TEXTURE    = 204005;     (not used)
    integer SET_THICKNESS      = 204006;  
//    integer SET_THICKNESS      = 204006;     (not used)
    integer SET_COLOR          = 204007;  
//    integer SET_COLOR          = 204007;     (not used)


     llMessageLinked(LINK_SET,DISPLAY_STRING,message,line);
     llMessageLinked(LINK_SET,DISPLAY_STRING,message,line);
}
}


// Clear the Xytext display
clear_display()  
clear_display()  
{
{
Line 130: Line 171:
}
}


// Make a menu / dialog
make_menu(key id)
make_menu(key id)
{
{
Line 156: Line 198:
}
}


// Make the menu option list for menu: catagories (genres)
list category_menu(integer num)
list category_menu(integer num)
{   // compile menu list of category list
{
    // if there are more catagories then can be displayed in the menu
    // compile a submenu, including options for main menu and more and a blank item.
     integer len=llGetListLength(category_list);
     integer len=llGetListLength(category_list);
     list menu=[];
     list menu=[];


     if (len > 9)
     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.  
         integer last_sub=(len-1)/9;  // submenus start at 0. 9th entry is in submenu 0, 10th in 1, etc.  
Line 205: Line 246:
}
}


// Returns the number of stations in a certain category
integer stations_in_category(integer cat)
integer stations_in_category(integer cat)
{
{
Line 219: Line 261:
}
}


// Not used currently -- to fix button placement
list order_buttons(list buttons)
list order_buttons(list buttons)
{
{
Line 239: Line 282:
}
}


// Returns a list of station names in a certain category (genre)
list station_list(integer category)
list station_list(integer category)
{
{
Line 253: Line 297:
}
}


// Returns the list of stations for the station menu, depending on the submenu number
list station_menu(integer num)
list station_menu(integer num)
{
{
  // compile menu list of station names
    // if there are more stations then can be displayed in the menu
    // compile a submenu, including options for main menu and more and a blank item.
     list stations=station_list(category_index);
     list stations=station_list(category_index);
     integer len=llGetListLength(stations);
     integer len=llGetListLength(stations);
     list menu=[];
     list menu=[];
    llSay(0,"Category: " + llList2String(category_list,category_index));
    llSay(0,"Stations: " + llDumpList2String(stations,","));
          
          
     if (len > 11)
     if (len > 11)       // 12 - 1 for MAIN menu
     {
     {
         integer last_sub=(len-1)/9;
         integer last_sub=(len-1)/9;
Line 306: Line 344:
}
}


// Returns whether av with key id has access
integer has_access(key id)
integer has_access(key id)
{
{
Line 323: Line 362:
}
}


// Gets a random channel -- uses a wide range of big negative channel numbers seldomly used
integer random_channel()
integer random_channel()
{
{
Line 331: Line 371:
}
}


// 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 check_url(string url)
{
{
Line 343: Line 386:


     string str_ip_port=llGetSubString(url,pos,-1);
     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_port=llParseString2List(str_ip_port,[":"],[]);                 // split in ip-adress and port
     list list_ip=llParseString2List(llList2String(list_ip_port,0),["."],[]);    // split ip-adress elements
     list list_ip=llParseString2List(llList2String(list_ip_port,0),["."],[]);    // split ip-adress elements


Line 368: Line 411:
}
}


// Returns a true value depending on the first character in input - anything else is assumed false.
integer true_value(string input)
integer true_value(string input)
{
{
Line 378: Line 422:
}
}


// Return if more input should be processed (if not at EOF) - sets ConfigError if any config error found.
integer process_line(string dataline)
integer process_line(string dataline)
{
{
Line 523: Line 568:
}
}


 
// Sets the parcel URL and updates the display
set_parcel_url(string url)
set_parcel_url(string url)
{
{
Line 546: Line 591:
}
}


// Returns if a category (genre) exists.
integer available_category(string category)
integer available_category(string category)
{
{
Line 558: Line 604:
}
}


// 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 empty_category(string category)
{
{
Line 570: Line 617:
}
}


// Removes categories (genres) for which no station is known.
skip_empty_categories()
skip_empty_categories()
{
{
Line 605: Line 653:
         menu_type=0;
         menu_type=0;
          
          
         if (llGetInventoryType(config_notecard) != INVENTORY_NONE)
         if (llGetInventoryType(config_notecard) == INVENTORY_NOTECARD)
         {
         {
           reqid=llGetNotecardLine(config_notecard,lineno++);
           reqid=llGetNotecardLine(config_notecard,lineno++);
Line 657: Line 705:
     changed(integer ch)
     changed(integer ch)
     {
     {
         if (ch & CHANGED_INVENTORY && flag)
         if (ch & CHANGED_INVENTORY)
             llResetScript();
             llResetScript();
     }
     }
Line 670: Line 718:
     state_entry()
     state_entry()
     {
     {
         llWhisper(0,"Reset on owner touch.");
         llWhisper(0,"Reset on owner touch or when notecard updated.");
     }
     }
          
          
     touch_start(integer t)
     touch_start(integer t)
  {
    {
         if (llDetectedKey(0)==llGetOwner())
         if (llDetectedKey(0)==llGetOwner())
            llResetScript();
    }
    changed(integer ch)
    {
        if (ch & CHANGED_INVENTORY && flag)
             llResetScript();
             llResetScript();
     }
     }
Line 854: Line 908:
             }
             }
         }     
         }     
    }
    changed(integer ch)
    {
        if (ch & CHANGED_INVENTORY && flag)
            llResetScript();
     }
     }
}
}
Line 899: Line 959:
# Shoutcast radio stations
# Shoutcast radio stations
# Use a '|' between each field - don't mind spaces before and after fields, they will be trimmed
# 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 url+port
# 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 in [GENRE]  
# 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
Classic Rock    | Absolute        |    Absolute Radio                  | http://205.188.215.226:8018
# etc.
# etc.
Line 907: Line 967:
===TODO===
===TODO===


* Add a llRegionSay to broadcast the station/genre/current songtitle to the region and write a script that displays that info on an Xytext board.
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

Revision as of 17:20, 9 February 2011

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
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