Shoutcast - radio controller v0.3 (remake of similar scripts)

From Second Life Wiki
Revision as of 08:59, 25 January 2015 by ObviousAltIsObvious Resident (talk | contribs) (<lsl> tag to <source>)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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. Credits for this script go to previous developers of similar scripts:

  • Scripter Coba who wrote the LandOwnersRadio (V2.0) script. Menu driven / notecard config script to select genre and station to set parcel music url.
  • Jamie Otis who wrote the Raven radio infoboard script. Shows what song is playing by fetching info using the sis.slserver.info/sis.php service (now offline). Display on Xytext.
  • Darkie Minotaur who wrote the currentPlaying script. Shows what song is currently playing by fetching info from the /7.html shoutcast page.

Although this script is based upon similar functionality as provided in these scripts, this script is not simply a merge/copy of those scripts, but a complete make-over. I use my own notecard format and processing of notecard info, and own menu system.

Versions

Current version: v0.3 released 10-2-2011

Scripts

Shoutcast - radio controller

This script should be put into the shoutcast - radio controller device.

// 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  (0 <= xxx <= 255) 
// * 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. Control buttons should be on the first line.
// * 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.)
/////////////////////////////////////////////////////////////////////////////////////////
// Future plans:
// * Individual user preferences that can be stored on seperate note cards. A user has access to his own list of genres and stations and the system available genres/stations.
// * Feature for accessing online playlists (M3U, PLS, other formats) to play a list of songs provided by that playlist.
// * User provided url.
/////////////////////////////////////////////////////////////////////////////////////////
// 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";                            ///////    EDITABLE  \\\\\\ 
string button_HELP = "HELP";                            ///////    EDITABLE  \\\\\\ 
string button_NEXT = ">>";                              ///////    EDITABLE  \\\\\\ 
string button_PREV = "<<";                              ///////    EDITABLE  \\\\\\ 
string button_ON   = "ON";                              ///////    EDITABLE  \\\\\\  
string button_OFF  = "OFF";                             ///////    EDITABLE  \\\\\\ 

//////////////////////////////////
// Don't touch the variables below 
/////////////////////////////////

// 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. Reading config card stops at the first error.
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 (i.e. there are no stations for this catagory (genre))
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)
            llResetScript();
    }
}

/////////////////////////////////////////////
// state menu
////////////////////////////////////////////

state menu
{
    state_entry()
    {
        menu_type=0;
        menu_num=0;
        listen_handle=0;
    }
    
    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)
            llResetScript();
    }
}

//////////////////////////////
// end of script
//////////////////////////////

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