// @ts-check
// Fusion Research Driver - Music Module - Library Browsing
//
// Copyright 2013-2021 Fusion Research, Inc.
// by Alexa Jade Fanti
//
// Comments:
//   This contains objects and data structures to support browsing the server's music
//   library. We have to keep track of a "history" of browsing (like browser breadcrumbs)
//   do the user can bo "back" a level without having to re-load the entire list.
//   We also need a separate instamce of this browse history for each "view" in the system!
//
// Depends: 
//   fusion_stringutils.js
//   fusion_misc.js
//   fusion_debugging.js
//   fusion_sys_config.js
//   fusion_http.js
//   fusion_url.js
//   fusion_keyboard.js
//
//System.Print("Fusion Driver - Music Module - Library Browsing loaded...\r\n");




/******************************************************************
 * START Library Global Variables and Functions
 ******************************************************************/
// I need a function that can be called from the keyboard to send us the view ID, path and search term
// This needs to get a browser (based on view) and then call the OpenTarget!
// Note: I depend on the global library manager object for this!
function Library_OnSearchComplete(view, path, search)
{
	DebugLibrary("OnSearchComplete... Search view " + view + " on path + '" + path + "' for '" + search + "'", g_DEBUG_LEVEL_INFO);

	var library = gLIBRARY.GetBrowser_fromViewID(view);
	library.OpenPath(path, search);
}

var g_LIBRARY_LIST_INITIAL_STEP  = ReadConfig_AsInt("List_Fetch_InitialSize", 20, 10, 100);
var g_LIBRARY_LIST_MULTIPLY_STEP = ReadConfig_AsInt("List_Fetch_Multiplier", 2, 1, 10);
var g_LIBRARY_LIST_MAXIMUM_STEP  = ReadConfig_AsInt("List_Fetch_MaximumSize", 500, 100, 1000);

var g_LIBRARY_SERVICE_ID_NONE           = 0;
var g_LIBRARY_SERVICE_ID_MY_MUSIC       = 1;
var g_LIBRARY_SERVICE_ID_INTERNET_RADIO = 2;
var g_LIBRARY_SERVICE_ID_FAVORITES      = 3;
var g_LIBRARY_SERVICE_ID_AIRPLAY_2      = 4;
var g_LIBRARY_SERVICE_ID_BBC_SOUNDS     = 5;
var g_LIBRARY_SERVICE_ID_IHEARTRADIO    = 6;
var g_LIBRARY_SERVICE_ID_PANDORA        = 7;
var g_LIBRARY_SERVICE_ID_QOBUZ          = 8;
var g_LIBRARY_SERVICE_ID_SIRIUSXM       = 9;
var g_LIBRARY_SERVICE_ID_SOUNDMACHINE   = 10;
var g_LIBRARY_SERVICE_ID_SPOTIFY        = 11;

function Library_TopLevelServiceID(path)
{
	var source = g_LIBRARY_SERVICE_ID_NONE;
//DebugLibrary('TOP LEVEL SERVICE PATH = ' + path, g_DEBUG_LEVEL_INFO);
	if (path.startsWith('/local'))    { source = g_LIBRARY_SERVICE_ID_MY_MUSIC;       } else
	if (path.startsWith('/radio'))    { source = g_LIBRARY_SERVICE_ID_INTERNET_RADIO; } else
	if (path.startsWith('/favorite')) { source = g_LIBRARY_SERVICE_ID_FAVORITES;      } else
	if (path.startsWith('/service/') || path.startsWith('/[service]/')) {
		if (path.includes('/fr_shairportsyncmb')) { source = g_LIBRARY_SERVICE_ID_AIRPLAY_2;    } else
		if (path.includes('/bbcsounds'))          { source = g_LIBRARY_SERVICE_ID_BBC_SOUNDS;   } else
		if (path.includes('/fr_iheartradio'))     { source = g_LIBRARY_SERVICE_ID_IHEARTRADIO;  } else
		if (path.includes('/fr_pandora_'))        { source = g_LIBRARY_SERVICE_ID_PANDORA;      } else
		if (path.includes('/qobuz'))              { source = g_LIBRARY_SERVICE_ID_QOBUZ;        } else
		if (path.includes('/fr_siriusxmu_'))      { source = g_LIBRARY_SERVICE_ID_SIRIUSXM;     } else
		if (path.includes('/fr_soundmachine_'))   { source = g_LIBRARY_SERVICE_ID_SOUNDMACHINE; } else
		if (path.includes('/fr_spotify_'))        { source = g_LIBRARY_SERVICE_ID_SPOTIFY;      } else
		if (path.includes('/spotty'))             { source = g_LIBRARY_SERVICE_ID_SPOTIFY;      } else
			source = 'Other';
	}
//DebugLibrary('TOP LEVEL SERVICE SOURCE = ' + source, g_DEBUG_LEVEL_INFO);
	return source;
}

function Library_TopLevelService(path)
{
    var sourceId = Library_TopLevelServiceID(path)
	switch (sourceId) {
	  case g_LIBRARY_SERVICE_ID_MY_MUSIC:	    return 'My Music';
	  case g_LIBRARY_SERVICE_ID_INTERNET_RADIO: return 'Internet Radio';
	  case g_LIBRARY_SERVICE_ID_FAVORITES: 	    return 'Favorites';
	  case g_LIBRARY_SERVICE_ID_AIRPLAY_2: 	    return 'Airplay 2';
	  case g_LIBRARY_SERVICE_ID_BBC_SOUNDS:	    return 'BBC Sounds';
	  case g_LIBRARY_SERVICE_ID_IHEARTRADIO:    return 'iHeartRadio';
	  case g_LIBRARY_SERVICE_ID_PANDORA: 	    return 'Pandora';
	  case g_LIBRARY_SERVICE_ID_QOBUZ:  	    return 'Qobuz';
	  case g_LIBRARY_SERVICE_ID_SIRIUSXM: 	    return 'SiriusXM';
	  case g_LIBRARY_SERVICE_ID_SOUNDMACHINE:   return 'SoundMachine';
	  case g_LIBRARY_SERVICE_ID_SPOTIFY:		return 'Spotify';
	  default: return '';
	}

//	var source = '';
//	if (path.startsWith('/local'))    { source = 'My Music';    } else
//	if (path.startsWith('/radio'))    { source = 'Internet Radio'; } else
//	if (path.startsWith('/favorite')) { source = 'Favorites';      } else
//	if (path.startsWith('/service/') || path.startsWith('/[service]/')) {
//		if (path.includes('/fr_shairportsyncmb')) { source = 'Airplay 2';    } else
//		if (path.includes('/bbcsounds'))          { source = 'BBC Sounds';   } else
//		if (path.includes('/fr_iheartradio'))     { source = 'iHeartRadio';  } else
//		if (path.includes('/fr_pandora_'))        { source = 'Pandora';      } else
//		if (path.includes('/qobuz'))              { source = 'Qobuz';        } else
//		if (path.includes('/fr_siriusxmu_'))      { source = 'SiriusXM';     } else
//		if (path.includes('/fr_soundmachine_'))   { source = 'SoundMachine'; } else
//		if (path.includes('/fr_spotify_'))        { source = 'Spotify';      } else
//		if (path.includes('/spotty'))             { source = 'Spotify';      } else
//			source = 'Other';
//	}
//DebugLibrary('TOP LEVEL SERVICE SOURCE = ' + source, g_DEBUG_LEVEL_INFO);
//	return source;
}
/******************************************************************
 * END   Library Global Variables and Functions
 ******************************************************************/


/******************************************************************
 * START Library Entry Object
 ******************************************************************/
function LibraryEntry(owningLibraryLocation,
					  name, 
	                  path,
	                  art, 
	                  artist, 
	                  album, 
	                  genre, 
	                  year, 
	                  duration, 
	                  bitrate, 
	                  tracknum, 
	                  favoriteUrl, 
					  favoriteArtUrl,
	                  favoriteId,
	                  playlistId,
	                  playlistItemIndex,
	                  canOpen, 
	                  canPlayLoad, 
	                  canPlayAdd, 
	                  canPlayInsert, 
	                  canSearch,
	                  canAddFavorite,
	                  canRemoveFavorite,
	                  canDeletePlaylist,
	                  canRemovePlaylistItem)
{
	this.Initialize();
	this._owner = owningLibraryLocation; // The LibraryLocation that owns this entry!

	if (name              !== undefined) try { this.name              = System.ConvertFromUTF8(name.toString()); } catch(err) { }
	if (path              !== undefined) try { this.path              = System.ConvertFromUTF8(path.toString()); } catch(err) { }
	if (art               !== undefined) try { this.art               = art.toString();                  } catch(err) { }
	
//	if (artist            !== undefined) try { this.artist            = artist.toString();               } catch(err) { }
//	if (album             !== undefined) try { this.album             = album.toString();                } catch(err) { }
//	if (genre             !== undefined) try { this.genre             = genre.toString();                } catch(err) { }
	if (artist            !== undefined) try { this.artist            = System.ConvertFromUTF8(artist.toString());               } catch(err) { }
	if (album             !== undefined) try { this.album             = System.ConvertFromUTF8(album.toString());                } catch(err) { }
	if (genre             !== undefined) try { this.genre             = System.ConvertFromUTF8(genre.toString());                } catch(err) { }
	
	if (year              !== undefined) try { this.year              = parseInt(year, 10);              } catch(err) { }
	if (duration          !== undefined) try { this.duration          = parseInt(duration, 10);          } catch(err) { }
	
	if (bitrate           !== undefined) try { this.bitrate           = bitrate.toString();              } catch(err) { }
	if (tracknum          !== undefined) try { this.tracknum          = parseInt(tracknum, 10);          } catch(err) { }
	if (favoriteUrl       !== undefined) try { this.favoriteUrl       = favoriteUrl.toString();          } catch(err) { }
	if (favoriteArtUrl    !== undefined) try { this.favoriteArtUrl    = favoriteArtUrl.toString();       } catch(err) { }
	if (favoriteId        !== undefined) try { this.favoriteId        = favoriteId.toString();           } catch(err) { }

	if (playlistId        !== undefined) try { this.playlistId        = playlistId.toString();           } catch(err) { }
	if (playlistItemIndex !== undefined) try { this.playlistItemIndex = parseInt(playlistItemIndex, 10); } catch(err) { }

	if (canOpen           !== undefined) try { this.canOpen           = parseBool(canOpen);              } catch(err) { }
	if (canPlayLoad       !== undefined) try { this.canPlayLoad       = parseBool(canPlayLoad);          } catch(err) { }
	if (canPlayAdd        !== undefined) try { this.canPlayAdd        = parseBool(canPlayAdd);           } catch(err) { }
	if (canPlayInsert     !== undefined) try { this.canPlayInsert     = parseBool(canPlayInsert);        } catch(err) { }
	if (canSearch         !== undefined) try { this.canSearch         = parseBool(canSearch);            } catch(err) { }
	if (canAddFavorite    !== undefined) try { this.canAddFavorite    = parseBool(canAddFavorite);       } catch(err) { }
	if (canRemoveFavorite !== undefined) try { this.canRemoveFavorite = parseBool(canRemoveFavorite);    } catch(err) { }

	if (canDeletePlaylist !== undefined) try { this.canDeletePlaylist = parseBool(canDeletePlaylist);    } catch(err) { }
	if (canRemovePlaylistItem !== undefined) try { this.canRemovePlaylistItem = parseBool(canRemovePlaylistItem); } catch(err) { }
}


LibraryEntry.prototype.Initialize = function()
{
	this.name              = "";
	this.path              = "";
	this.art               = "";
	this.artist            = "";
	this.album             = "";
	this.genre             = "";
	this.year              = 0;
	this.duration          = 0;
	this.bitrate           = 0;
	this.tracknum          = 0;
	this.favoriteUrl       = "";
	this.favoriteArtUrl    = "";
	this.favoriteId        = "";
	this.playlistId        = "";
	this.playlistItemIndex = -1;
	this.canOpen           = false;
	this.canPlayLoad       = false;
	this.canPlayAdd        = false;
	this.canPlayInsert     = false;
	this.canSearch         = false;
	this.canAddFavorite    = false;
	this.canRemoveFavorite = false;
	this.canDeletePlaylist = false;
	this.canRemovePlaylistItem = false;
}

LibraryEntry.prototype.toString = function()
{
//	return this.name + " by " + this.artist + " from " + this.album + " [" + this.duration + "] (" + this.path + ")";
	return this.name + " by " + this.artist + " from " + this.album + " [" + this.duration + "] (" + this.path + ") Art = " + this.art;
}


/******************************************************************
 * END   Library Entry Object
 ******************************************************************/


/******************************************************************
 * START Library Location Object
 ******************************************************************/
function LibraryLocation(owningLibraryBrowser, playerMac)
{
	this.Initialize();
	this._owner = owningLibraryBrowser;
	this._playerMac = playerMac;

	this._openedIndex = -1; // 0; 
	// This is the index of the entry we are opening if we're descending!
	// We should ONLY be set by the Browser as a way to track what's happening!
	this._openedWindowTop = 0;
	// This is the top of the list window when we selected the index...
}

LibraryLocation.prototype.Initialize = function()
{
	DebugLibrary("Location (" + this.toString() + ") Initialize...", g_DEBUG_LEVEL_TRACE);
	this._fetch_index  = 0;
	this._fetch_step   = g_LIBRARY_LIST_INITIAL_STEP;
	this._fetch_path   = "";
	this._fetch_search = "";

	this.recordName();
	this.recordPath();
	this.recordParent();
	this.recordEntries();

	this.dirty = false; // set this to true when we know the loaded data is no longer valid!

	this._openedIndex = -1; // 0; 
	// This is the index of the entry we are opening if we're descending!
	// We should ONLY be set by the Browser as a way to track what's happening!
	this._openedWindowTop = 0;
	// This is the top of the list window when we selected the index...

}

LibraryLocation.prototype.toString = function()
{
	if (this.path == this._fetch_path) {
		return "Loc " + this.name + " [" + this.path + "] (" + this.getEntryCount() + ")";
	} else {
		return "Loc " + this.name + " [" + this.path + " -> " + this._fetch_path + "] (" + this.getEntryCount() + ")";
	}
}

//==============> START  Setter Methods
LibraryLocation.prototype.recordName = function(name)
{
	if ((name === undefined) || (name === null)) name = "";
	this.name = name.toString();
}

LibraryLocation.prototype.recordPath = function(path)
{
	if ((path === undefined) || (path === null)) path = "/";
	this.path = path.toString();
}

LibraryLocation.prototype.recordParent = function(parent)
{
	if ((parent === undefined) || (parent === null)) parent = "/";
	this.parent = parent.toString();
}

LibraryLocation.prototype.recordEntries = function(entries)
{
	if ((entries !== undefined) && (entries !== null) && (entries instanceof Array)) {
		DebugLibrary("Location (" + this.toString() + ") record " + entries.length + " entries...", g_DEBUG_LEVEL_TRACE);
		var start = this.entries.length;
		var count = entries.length;
		
		for(var i = 0; i < entries.length; i++) this.entries.push(entries[i]);

		// 2022-01-21 - AJF
		// This is a hack for Spotify Searching... RTI wants a "New Search" to specify the term in the location name.
		// To do this we'll look for a location path of "/service/spotty=1.0" WITH a Name of "New Search" AND entries...
		// If we find all three... we'll look at the path of the FIRST entry and see if it looks like: 
		//  /service/spotty=1.0_[term].#
		// If it does, we extract the term and create a new title from that... this mimich the "previous search" names.
		
		// Regex to detect the New Search location...
		var regexp1 = /^\/\[service\]\/spotty=[0-9+]\.0$|^\/service\/spotty=[0-9+]\.0$|^\/app\/spotty=[0-9+]\.0$/g;
		var match1 = this.path.match(regexp1);
		if ( 
		    ((match1 !== null) && (match1.length = 1))
			&& (this.name == 'New Search') && (this.entries.length > 0)){
			DebugLibrary("Recording entries... this is a Spotty New Search with entries...", g_DEBUG_LEVEL_TRACE);
			
			// Let's see if there's a search term in the first entry!
			var regexp2 = /\/spotty=[0-9+]\.0_(.+)\.[0-9+]/;
			var match2 = this.entries[0].path.match(regexp2);
			if ((match2 !== null) && (match2.length == 2)) {
				var searchfor = decodeURIComponent(match2[1]);
				if (searchfor !== '') {
					DebugLibrary('Recording entries... changing location name to "' + searchfor +'"', g_DEBUG_LEVEL_TRACE);
					this.name = searchfor;
				}
			}
		}
 
		if (this._owner !== undefined) this._owner.OnFetchProgress(this, start, count);
	} else {
		DebugLibrary("Location (" + this.toString() + ") record 0 entries. (cleared)", g_DEBUG_LEVEL_TRACE);
		this.entries = new Array();
	}
}

LibraryLocation.prototype.makeDirty = function()
{
	this.dirty = true;
	if (this._owner !== undefined) this._owner.OnLocationDirty(this);
}
//==============> END    Setter Methods

//==============> START  Getter Methods
LibraryLocation.prototype.getPlayerMac = function()
{
	if (this._playerMac == '') this._playerMac = g_SERVER.GetAnyPlayerMAC();
	return this._playerMac;
}

LibraryLocation.prototype.getEntry = function(index)
{
	try {
		if (index < 0) return null;
		if (index >= this.entries.length) return null;
		return this.entries[index];
	} catch(err) {
		DebugLibrary("Location (" + this.toString() + ") getEntry error = " + err, g_DEBUG_LEVEL_ERROR);
	}
}

LibraryLocation.prototype.getEntryCount = function()
{
	if ((this.entries === undefined) || (this.entries === null)) return 0;
	return this.entries.length;
}
//==============> END    Getter Methods


//==============> START  Event Notification


//==============> END    Event Notification


//==============> START  Data Retrieval Methods
// How do we actually fetch a complete list of entries for a location, considering 
// this requires a HTTP callback and repeated fetches to acquire the entire list.
// Unfortunately we can't give the method of an opbject as a callback for the HTTP 
// machine. It doesn't call with the proper object instance. Instead we have to give 
// it a global function and then have the global function extract the object from 
// the tag and then call a pre-defined method. In this case: LibraryLocation_Callback
//
// This is a callback used by the Library Location object when fetching it's list!
// It depends on the LibraryEntry Object being set in the "tag" and having a 
// InternalParseXML method!
function LibraryLocation_Callback(httpRequest, httpResponse)
{
    var xml = httpResponse.GetBody_AsXML();
    var me = httpRequest.GetTag();
    try {
	    me.InternalParseXML(xml);
    } catch (err) {
		// There was no callback to talk to!    	
    }
}

// This is eventually called by the HTTP engine when we have the response to a fetch request.
// We have to parse the XML and recode our information and trigger events as things happen.
LibraryLocation.prototype.InternalParseXML = function(xml)
{
	if (xml === undefined) {
		DebugLibrary("Location (" + this.toString() + ") InternalParse XML is missing", g_DEBUG_LEVEL_ERROR);
	}
	DebugPerformance("Library location (" + this.toString() + ") processing XML", g_DEBUG_LEVEL_INFO);
	
	var start = parseInt(xml.librarystats.@start.toString());
	if (isNaN(start)) start = 0;
	var total = parseInt(xml.librarystats.@count.toString());
	if (isNaN(total)) total = -1;

	try { this.recordName(xml.library.@name.toString());     } catch(err) { }
	try { this.recordPath(xml.library.@path.toString());     } catch(err) { }
	try { this.recordParent(xml.library.@parent.toString()); } catch(err) { }

	if ((this._owner !== undefined) && (start == 0)) this._owner.OnFetchStarted(this);

	try {
		var entries = new Array();
		var items = xml.library.entry;
		for (var x = 0; x < items.length(); x++) {
			var new_entry = new LibraryEntry(this,
											 items[x].@name,
											 items[x].@path,
											 items[x].@art,
											 items[x].@artist,
											 items[x].@album,
											 items[x].@genre,
											 items[x].@year,
											 items[x].@duration,
											 items[x].@bitrate,
											 items[x].@tracknum,
											 items[x].@favorite_url,
											 items[x].@favorite_art_url,
											 items[x].@favorite_id,
											 items[x].@playlist_id,
											 items[x].@playlist_item_index,
											 items[x].@can_open,
											 items[x].@can_play_load,
											 items[x].@can_play_add,
											 items[x].@can_play_insert,
											 items[x].@can_search,
											 items[x].@can_add_favorite,
											 items[x].@can_remove_favorite,
											 items[x].@can_delete_playlist,
											 items[x].@can_remove_playlist_item);

//DebugLibrary("Read Entry " + x + ": " + new_entry.toString(), g_DEBUG_LEVEL_TRACE);

			entries.push(new_entry);
		}

		DebugPerformance("Library Location (" + this.toString() + ") recording entries...", g_DEBUG_LEVEL_INFO);
		this.recordEntries(entries);
		DebugPerformance("Library Location (" + this.toString() + ") recording of entries complete.", g_DEBUG_LEVEL_INFO);

		var have_count = this.getEntryCount();
		if ((total > 0) && (have_count < total) && (entries.length > 0)) {
			this._fetch_index = have_count;
			this._fetch_step  = this._fetch_step * g_LIBRARY_LIST_MULTIPLY_STEP;
			if (this._fetch_step > g_LIBRARY_LIST_MAXIMUM_STEP) this._fetch_step = g_LIBRARY_LIST_MAXIMUM_STEP;

//DebugLibrary("about to fetch again with index = " + this._fetch_index + " and step = " + this._fetch_step, g_DEBUG_LEVEL_TRACE);

			this.InternalFetch();
		} else if (this._owner !== undefined) {
			this.dirty = false; // We are no longer dirty!
			this._owner.OnFetchComplete(this, this._openedIndex, this._openedWindowTop);
		}

	} catch(err) {
		DebugLibrary("Location InternalParseXML failed with error: " + err, g_DEBUG_LEVEL_ERROR);
		if (this._owner !== undefined) {
			this.dirty = false; // We are no longer dirty!
			this._owner.OnFetchComplete(this, this._openedIndex, this._openedWindowTop);
		}
	}
}

LibraryLocation.prototype.InternalFetch = function()
{
	// This makes a fetch request based entirely off of the internal fetch variables;

   	var url = GetURL_ServerRequestLibrary("browse", 
   		                                  //this._playerMac, 
										  this.getPlayerMac(),
   		                                  this._fetch_path, 
   		                                  this._fetch_index, 
   		                                  this._fetch_step, 
   		                                  this._fetch_search);

	DebugLibrary("Location (" + this.toString() + ") asking for " + url, g_DEBUG_LEVEL_TRACE);
	DebugPerformance("Fetching Library Location (" + this.toString() + ") : " + url, g_DEBUG_LEVEL_INFO);

	var httpReq = new HttpRequest(url);
	httpReq.SetTag(this);
	httpReq.SetCallback(LibraryLocation_Callback);

// Note: 2021-08-18 - AJF
// If I want to be able to cancel a location's fetch I'll need to know whis and 
// set the callback to null to prevent the callback from happening!
//	this.activeHttp = httpReq; // Make a note of this right here!!!

	httpReq.Fetch();
}

LibraryLocation.prototype.Fetch = function(path, search)
{
	this.Initialize();

	if ((path === undefined) || (path === null)) {
		DebugLibrary("Location (" + this.toString() + ") Fetch path is missing, using /", g_DEBUG_LEVEL_INFO);
		path = "/";
	}

	if ((search === undefined) || (search === null)) search = "";
	DebugLibrary("Location (" + this.toString() + ") Fetch '" + path + "' with search = '" + search + "'", g_DEBUG_LEVEL_TRACE);

	// Configure future requests...
	this._fetch_path   = path;
	this._fetch_search = search;

	this.InternalFetch();
}

LibraryLocation.prototype.Refetch = function()
{
	DebugLibrary("Location (" + this.toString() + ") Re-fetching...", g_DEBUG_LEVEL_TRACE);
	this.Fetch(this._fetch_path, this._fetch_search);

	// Tell our browser they need to re-thing their current target!
	if (this._owner !== undefined) {
		DebugLibrary("Location (" + this.toString() + ") Re-fetch selecting new target using " + this.toString(), g_DEBUG_LEVEL_TRACE);
		this._owner.selectNewTarget(this);
	}
}
//==============> END    Data Retrieval Methods

/******************************************************************
 * END   Library Location Object
 ******************************************************************/


/******************************************************************
 * START Library Browser Object
 ******************************************************************/
function LibraryBrowser(viewID, playerMac)
{
	this._viewID = viewID; // This is the view ID for RTI... this is what we append to the library variables to make them view specific!
	this._playerMac = playerMac;
	// Set up the list variable!
	this._rti_list = new SystemVarsList("SysVar_Library_L_Entries%" + this._viewID);

	this.busy = false; 
	// We set this to true when a location is loading and ignore navigation requests while it's busy!
	
	DebugLibrary("Browser created with player MAC = '" + this._playerMac + "' and RTI System View = '" + this._viewID + "'", g_DEBUG_LEVEL_INFO);
	this.Initialize();
}

LibraryBrowser.prototype.Initialize = function()
{
	this.locations = new Array();
	this.recordTarget(); // This sets a this.target which must be null or a valid LibraryEntry
}

LibraryBrowser.prototype.toString = function()
{
	return this.getID();
}

//==============> START  Setter Methods
LibraryBrowser.prototype.setBusy = function(value)
{
	var value = parseBool(value);
	if (this.busy == value) return; // We don't double-set the value!

	this.busy = parseBool(value);
	// If we're setting busy to true, we should probably set a timer to get us out 
	// of it eventually incase something goes wrong and we never complete the fetch 
	// of the location.
	if (this.busy) {
		DebugLibrary(this.getID() + " is busy...", g_DEBUG_LEVEL_TRACE);
		SysVar_WriteBool("SysVar_Library_B_Busy%" + this._viewID, true);
		this.busyAt = System.GetLocalTimeInSeconds();
	} else {
		DebugLibrary(this.getID() + " is no longer busy.", g_DEBUG_LEVEL_TRACE);
		SysVar_WriteBool("SysVar_Library_B_Busy%" + this._viewID, false);
		this.busyAt = 0; // NOT busy!
	}
}
//==============> END    Setter Methods


//==============> START  Getter Methods
LibraryBrowser.prototype.getID = function()
{
	return "Browser (" + this._viewID + ")";
}

LibraryBrowser.prototype.getBusy = function()
{
	// Let's "timeout" of being busy if we're busy too long!
	if (this.busy) {
		DebugLibrary(this.getID() + " is busy... let's check for how long...", g_DEBUG_LEVEL_TRACE);
		// This should only happen when the user wants to navigate but we're busy...
		// Let's see if we should time out here then?

// DEBUG Fix this after testing!
		var timeout = ReadConfig_AsInt("Library_FetchTimeout_InSec", 5); // in seconds
		//var timeout = ReadConfig_AsInt("Library_FetchTimeout_InSec", 5) * 1000; // in seconds
		if (timeout <= 0) {
			DebugLibrary(this.getID() + " There is not 'busy timeout' for the browser... and that's probably a mistake!", g_DEBUG_LEVEL_WARN);
			return false; // We literally have NO TIMEOUT!
		}

		var now = System.GetLocalTimeInSeconds();
		//var now = this.busyAt = System.GetLocalTimeInSeconds();
		var busySince = this.busyAt + timeout;
		DebugLibrary(this.getID() + " has been busy since " + busySince, g_DEBUG_LEVEL_TRACE);
		DebugLibrary(this.getID() + " it is currently " + now, g_DEBUG_LEVEL_TRACE);


		if (busySince > now) {
			DebugLibrary(this.getID() + " has been busy too long. [busy at = " + this.busyAt + ", timeout = " + timeout + ", (total = " + busySince + ") and now it's " + now + "]", g_DEBUG_LEVEL_WARN);

//			var currentLoc = this.getCurrentLocation();
//			if ((currentLoc !== undefined) &&  (currentLoc !== null)) {
//				var http = currentLoc.activeHttp;
//				if ((http !== undefined) &&  (http !== null)) {
//					http.SetCallback(null);
//					DebugLibrary(this.getID() + " Removing callback on http object of current location (cancelling fetch)", g_DEBUG_LEVEL_TRACE);
//				} else {
//					DebugLibrary(this.getID() + " trying to cancel fetch but current location has NO HTTP object", g_DEBUG_LEVEL_WARN);
//				}
//			} else {
//				DebugLibrary(this.getID() + " trying to cancel fetch but NO CURRENT LOCATION", g_DEBUG_LEVEL_WARN);
//			}
// Not working... are we losing the current location?

//			this.setBusy(false);		
		}
	}

	return this.busy;
}

LibraryBrowser.prototype.getCurrentLocation = function()
{
	if ((this.locations === undefined) || (this.locations.length == 0)) {
		DebugLibrary(this.getID() + " does not have a current location", g_DEBUG_LEVEL_WARN);
		return undefined;
	}

	var loc = this.locations[this.locations.length -1];
	DebugLibrary(this.getID() + " current location is " + loc.toString(), g_DEBUG_LEVEL_TRACE);
	return loc;

	//return this.locations[this.locations.length -1];
}

LibraryBrowser.prototype.getCurrentLevel = function()
{
	if (this.locations === undefined) {
		DebugLibrary(this.getID() + " does not have a current level", g_DEBUG_LEVEL_INFO);
		return -1; // We have NO locations!
	}

	var level = this.locations.length -1;
	DebugLibrary(this.getID() + " current level is " + level, g_DEBUG_LEVEL_INFO);
	return level;

	//return this.locations.length -1;
}
//==============> END    Getter Methods


//==============> START  Location Event Handlers
// Called by the location when it receives its first XML response (which may also be its last)
LibraryBrowser.prototype.OnFetchStarted = function(location)
{ 
	this.setBusy(true); // If we wern't already busy... we are now!
	if (location === undefined) {
		DebugLibrary(this.getID() + " OnFetchStarted called with UNDEFINED location", g_DEBUG_LEVEL_CRITICAL);
		return;
	}

	// Let's clear the RTI list...
	this._rti_list.Open();
	this._rti_list.RemoveAll();
	this._rti_list.Close();

	SysVar_Write("SysVar_Library_S_LocationSource%" + this._viewID, Library_TopLevelService(location.path));
	SysVar_Write("SysVar_Library_S_LocationName%" + this._viewID, location.name);

	DebugLibrary(this.getID() + " -------------------------------------- @ " + (this.locations.length -1), g_DEBUG_LEVEL_INFO);
	DebugLibrary(this.getID() + " Location started to fetch " + location.name, g_DEBUG_LEVEL_INFO);
}

// Called by the location as it parses entries in its location list. 
// Index is the first new entry and count is the number of entries we have retrieved.
LibraryBrowser.prototype.OnFetchProgress = function(location, index, count)
{
	if (location === undefined) {
		DebugLibrary(this.getID() + " OnFetchProgress called with UNDEFINED location", g_DEBUG_LEVEL_CRITICAL);
		return;
	}

	DebugLibrary("Location retrieved entries " + location.name + " from " + index + " to " + (index +count -1), g_DEBUG_LEVEL_INFO);

	// Let's append to the RTI list...
	this._rti_list.Open();
	for (var x = 0; x < count; x++) {
		DebugLibrary(this.getID() + " Read Entry " + x + ": " + location.entries[index + x].toString(), g_DEBUG_LEVEL_TRACE);

		var imageUrl = location.entries[index + x].art;
		if ((g_SERVER.view_player_map.IsViewImageCapable(this._viewID)) && (imageUrl != '') && g_SERVER.CanDisplayListArt()) {
			DebugLibrary('List item "' + location.entries[index + x].name + '" art = ' + imageUrl, g_DEBUG_LEVEL_TRACE);
			imageUrl = GetURL_CompleteArt(imageUrl);
			DebugLibrary('List item "' + location.entries[index + x].name + '" art is NOW = ' + imageUrl, g_DEBUG_LEVEL_TRACE);
			DebugArt("Inserting list art URL = '" + imageUrl + "'' for item = " + location.entries[index + x].name, g_DEBUG_LEVEL_TRACE);
			this._rti_list.InsertWithImage(location.entries[index + x].name, imageUrl);
		} else {
			this._rti_list.Insert(location.entries[index + x].name);
		}
	}
	this._rti_list.Close();

	// Rewriting the name in case it's changed dus to the Spotty Search hack!
	SysVar_Write("SysVar_Library_S_LocationName%" + this._viewID, location.name);
}

// Called by the location when it has retrieved all possible entries.
// Index it the entry we should navigate to 
LibraryBrowser.prototype.OnFetchComplete = function(location, index, windowtop)
{
	if (location === undefined) {
		DebugLibrary(this.getID() + " OnFetchComplete called with UNDEFINED location", g_DEBUG_LEVEL_CRITICAL);
		return;
	}

	DebugLibrary("Location completed " + location.name + " with " + location.entries.length + " (index = " + index + ")", g_DEBUG_LEVEL_INFO);
	DebugLibrary(this.getID() + " =================================== @ " + (this.locations.length -1), g_DEBUG_LEVEL_INFO);

	SysVar_Write("SysVar_Library_I_LocationCount%" + this._viewID, location.getEntryCount());

	// Let's scroll to where we want to in the RTI list...
	this._rti_list.Open();
	this._rti_list.SetIndexes(index, windowtop);
	this._rti_list.Close();

	this.setBusy(false);
}

LibraryBrowser.prototype.OnLocationDirty = function(location)
{
	// We were just informed a location became dirty... We need to know if it's the current location!
	var current = this.getCurrentLocation();
	if (location == current) {
		DebugLibrary(location.toString() + " is the current location and is NOW DIRTY!!!", g_DEBUG_LEVEL_INFO);
		current.Refetch(); // This handles selecting a new target!
	} else {
		DebugLibrary(location.toString() + " is dirty...", g_DEBUG_LEVEL_INFO);
	}
}

//==============> END    Location Event Handlers

//==============> START Target Operations
LibraryBrowser.prototype.getTarget = function()
{
	return this.target;
}

LibraryBrowser.prototype.reportTarget = function()
{
	if ((this.target === undefined) || (this.target == null)) {
    	SysVar_Write("SysVar_Library_S_Target_Name%" + this._viewID,                  "");
		SysVar_Write("SysVar_Library_S_Target_Artist%" + this._viewID,                "");
		SysVar_Write("SysVar_Library_S_Target_Album%" + this._viewID,                 "");
		SysVar_Write("SysVar_Library_S_Target_Genre%" + this._viewID,                 "");
		SysVar_WriteInt ("SysVar_Library_I_Target_Duration%" + this._viewID,              0);
		SysVar_Write    ("SysVar_Library_S_Target_Duration_AsString%" + this._viewID,     "");
		SysVar_WriteBool("SysVar_Library_B_Target_CanOpen%" + this._viewID,               false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanPlayLoad%" + this._viewID,           false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanPlayAdd%" + this._viewID,            false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanPlayInsert%" + this._viewID,         false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanAddFavorite%" + this._viewID,        false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanRemoveFavorite%" + this._viewID,     false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanRenamePlaylist%" + this._viewID,     false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanDeletePlaylist%" + this._viewID,     false);
		SysVar_WriteBool("SysVar_Library_B_Target_CanRemovePlaylistItem%" + this._viewID, false);
		SysVar_WriteArt ("SysVar_Library_A_Target_Art%" + this._viewID,                   "");
	} else {
    	SysVar_Write("SysVar_Library_S_Target_Name%" + this._viewID,                  this.target.name);
		SysVar_Write("SysVar_Library_S_Target_Artist%" + this._viewID,                this.target.artist);
		SysVar_Write("SysVar_Library_S_Target_Album%" + this._viewID,                 this.target.album);
		SysVar_Write("SysVar_Library_S_Target_Genre%" + this._viewID,                 this.target.genre);
		SysVar_WriteInt ("SysVar_Library_I_Target_Duration%" + this._viewID,              this.target.duration);
		SysVar_Write    ("SysVar_Library_S_Target_Duration_AsString%" + this._viewID,     SecondsToPrettyString_HMMSS(this.target.duration));
		SysVar_WriteBool("SysVar_Library_B_Target_CanOpen%" + this._viewID,               this.target.canOpen);
		SysVar_WriteBool("SysVar_Library_B_Target_CanPlayLoad%" + this._viewID,           this.target.canPlayLoad);
		SysVar_WriteBool("SysVar_Library_B_Target_CanPlayAdd%" + this._viewID,            this.target.canPlayAdd);
		SysVar_WriteBool("SysVar_Library_B_Target_CanPlayInsert%" + this._viewID,         this.target.canPlayInsert);
		SysVar_WriteBool("SysVar_Library_B_Target_CanAddFavorite%" + this._viewID,        this.target.canAddFavorite);
		SysVar_WriteBool("SysVar_Library_B_Target_CanRemoveFavorite%" + this._viewID,     this.target.canRemoveFavorite);
		SysVar_WriteBool("SysVar_Library_B_Target_CanRenamePlaylist%" + this._viewID,     this.target.canDeletePlaylist);
		SysVar_WriteBool("SysVar_Library_B_Target_CanDeletePlaylist%" + this._viewID,     this.target.canDeletePlaylist);
		SysVar_WriteBool("SysVar_Library_B_Target_CanRemovePlaylistItem%" + this._viewID, this.target.canRemovePlaylistItem);
		
		if ((this.target.art.length > 0) && 
		    (this.target.art.toLowerCase().substring(0, 7) !== "http://") &&
			(this.target.art.toLowerCase().substring(0, 8) !== "https://")) {
			SysVar_WriteArt("SysVar_Library_A_Target_Art%" + this._viewID, GetURL_ServerRequestEx("get_art", "", this.target.art, ""));
		} else {
			SysVar_WriteArt("SysVar_Library_A_Target_Art%" + this._viewID, this.target.art);
		}
	}
}

LibraryBrowser.prototype.recordTarget = function(entry)
{
	if ((entry !== undefined) && (entry != null)) {
		this.target = entry;
	} else {
		this.target = null;
	} 

	if (this.target != null) {
		DebugLibrary(this.getID() + " recordTarget as " + this.target.toString(), g_DEBUG_LEVEL_INFO);
	} else {
		DebugLibrary(this.getID() + " recordTarget as NOTHING", g_DEBUG_LEVEL_INFO);
	}

	this.reportTarget();
}

LibraryBrowser.prototype.selectNewTarget = function(location)
{
	DebugLibrary(this.getID() + " selectNewTarget...", g_DEBUG_LEVEL_TRACE);
	// Let's start off by assuming the current target is invalid!
	this.recordTarget();

	// Do I want to verify the location is in fact the "current" location in our list?
	if (this.locations.length > 0) {
		var currentloc = this.locations[this.locations.length -1];

		if (currentloc = location) {
			DebugLibrary(this.getID() + " selectNewTarget using current location of " + location.toString(), g_DEBUG_LEVEL_INFO);
		} else {
			DebugLibrary(this.getID() + " selectNewTarget NOT using current location... instead using " + location.toString(), g_DEBUG_LEVEL_WARN);
		}
	}

	// If we have any locations, we look at the current one...
	if ((location !== undefined) && (location != null)) {
		DebugLibrary(this.getID() + " selectNewTarget using location " + location.toString() + " which has an 'opened index' of " + location._openedIndex, g_DEBUG_LEVEL_TRACE);
		if (location._openedIndex > -1) this.recordTarget(location.entries[location._openedIndex]);
	}
}
//==============> END   Target Operations




LibraryBrowser.prototype.Home = function(index)
{
	if (this.getBusy()) {
		DebugLibrary(this.getID() + " cannot navigate 'Home' as it's currenty busy.", g_DEBUG_LEVEL_WARN);
		return;
	}

	// The user wants to "go home" which means drop everything and re-load the ROOT location
	DebugLibrary(this.getID() + " going 'Home'...", g_DEBUG_LEVEL_INFO);
	this.Initialize();
	this.Open(0);
}

LibraryBrowser.prototype.Back = function(index)
{
	if (this.getBusy()) {
		DebugLibrary(this.getID() + " cannot navigate 'Back' as it's currenty busy.", g_DEBUG_LEVEL_WARN);
		return;
	}

	// The user wants to go "back" a level. If we're at the top, we want to drop it and re-load 
	// from scratch. This makes sure we always have a "fresh" view if they go back up far enough.
	var loc = this.locations.pop(); // Forget the current location...
// DEBUG We are changing the location list here
	
	// If we have no more locations, we start from scratch at the ROOT
	if (this.getCurrentLevel() < 0) {
		this.Initialize();
		this.Open(0);
		return;
	}

	// If we get here, we have something to go back to...
	loc = this.getCurrentLocation();
	DebugLibrary(this.getID() + " going 'Back' to " + loc.toString() + " [level = " + this.getCurrentLevel() + "]", g_DEBUG_LEVEL_INFO);


	if (loc.dirty) {
		DebugLibrary(this.getID() + " " + loc.toString() + " is DIRTY so we'll re-fetch!", g_DEBUG_LEVEL_INFO);
		loc.Refetch();
		return;
	}

	this.OnFetchStarted(loc);
	this.OnFetchProgress(loc, 0, loc.entries.length);
	this.OnFetchComplete(loc, loc._openedIndex, loc._openedWindowTop);

	this.selectNewTarget(loc);
}

LibraryBrowser.prototype.Open = function(index, search, windowtop, playerIndex, view)
{
//System.Print("0.1");
	index = parseInt(index, 10);
	if (isNaN(index)) {
		DebugLibrary(this.getID() + " Open: can't open to an unspecified index... assuming 0.", g_DEBUG_LEVEL_WARN);
		index = 0;
	}
	if (search === undefined) search = "";

//System.Print("0.2");
	if (this.getBusy()) {
		DebugLibrary(this.getID() + " Open: cannot navigate 'Open(" + index + ")' as it's currenty busy.", g_DEBUG_LEVEL_WARN);
		return;
	}
//System.Print("0.3");

	
	// This should look at the last location in the array and find the entry by index.
	// Then create a new location, append it to the array and fetch it's stuff based on the location.
	// It must also recire the "opened" index as the position to return to...

	// If we don't have any locations, we need to start at the root!
	if (this.getCurrentLevel() < 0) {
//System.Print("0.4");
		DebugLibrary(this.getID() + " Open: can't open anything... starting at root.", g_DEBUG_LEVEL_INFO);
		
		// Create a new location with this parent and it's player MAC...
		var loc = new LibraryLocation(this, this._playerMac);
//System.Print("0.5");
		
		// Add the location to our array (it'll be the current location now!)
		this.locations.push(loc);
// DEBUG We are changing the location list here
//System.Print("0.6");
		
		// Start the fetch process for the location
		this.setBusy(true);
		loc.Fetch(); // Start at the root!
//System.Print("0.7");

		return;
	}

	// If we get here we have something to open...
//System.Print("1");
	var loc = this.getCurrentLocation();
//System.Print("2");
	if ((index >= 0) && (index < loc.entries.length)) {
//System.Print("3");
		loc._openedIndex = index; // Track what we've opened!
		loc._openedWindowTop = windowtop; // Track what we've opened!

		var entry = loc.entries[index];
		DebugLibrary(this.getID() + " Open: Opening " + loc.toString() + " entry " + entry.toString() + "  [level = " + this.getCurrentLevel() + "]", g_DEBUG_LEVEL_INFO);

		if ((entry.canSearch) && (search == "")) {
			DebugLibrary(this.getID() + " Open: '" + entry.path + "' must be searched, but we don't have a search term yet...", g_DEBUG_LEVEL_INFO);
			getKeyboard_fromViewID_andOpen(this._viewID, "", 'Library_OnSearchComplete(' + this._viewID + ', "' + entry.path + '", "$1")');
			return;
		}

		var action = ReadConfig_AsString("Library_SelectAction", "");
		var entry_canOpen = entry.canOpen;

		// Handle our Select Action configuration here...
		if (((entry.path.startsWith('/service/fr_pandora_')) || (entry.path.startsWith('/[service]/fr_pandora_'))) && (entry.name == 'Play this station')) {
			DebugLibrary(this.getID() + " The user is 'opening' a Pandora 'Play this station' entry... we need to assert this is not openable and our action os 'load'", g_DEBUG_LEVEL_INFO);
			entry_canOpen = false; // We'll claim pandora can't open if it can play/load! Forcing a play event!
			action = 'load'; // We always want to load this no matter what the user wants!!!
		}

//System.Print("4");
 		if (!entry_canOpen) {
	 		if (entry.canPlayLoad) {
	 			this.recordTarget(entry);
 				DebugLibrary(this.getID() + "  Open: '" + entry.path + "' can't be opened and MUST be played... (configured action = '" + action + "')", g_DEBUG_LEVEL_INFO);

				if (action == 'nothing') {
			 		DebugLibrary(this.getID() + " Open: Our action is 'nothing' so we're exiting here!", g_DEBUG_LEVEL_TRACE);
        	        return;
            	}

	            var queue = g_SERVER.GetQueueByIndexOrView(playerIndex, view);
    	        if (queue !== undefined) {

        	        if (action == 'smartadd') {
            	        // Look at the path and select a new action 
			 			DebugLibrary(this.getID() + " Open: Our action is 'smartadd' so we're gonna look at the path and figure out what to do!", g_DEBUG_LEVEL_TRACE);

						if (entry.path.startsWith('/radio')) {
							DebugLibrary(this.getID() + " Open: Our path starts with '/radio' so we'll 'load'", g_DEBUG_LEVEL_TRACE);
							action = 'load';
						} else 
						if (entry.path.startsWith('/favorite')) {
							DebugLibrary(this.getID() + " Open: Our path starts with '/favorite' so we'll 'load'", g_DEBUG_LEVEL_TRACE);
							action = 'load';
						} else 
						if ((entry.path.startsWith('/service')) || (entry.path.startsWith('/[service]'))) {
							DebugLibrary(this.getID() + " Open: Our path starts with '/service' so we have to look closer...", g_DEBUG_LEVEL_TRACE);
							if ((entry.path.includes('pandora')) || (entry.path.includes('sirius')) || (entry.path.includes('soundmachine'))) {
								DebugLibrary(this.getID() + " Open: Our path contains 'pandora', 'sirius' or 'soundmachine' so we'll 'load'", g_DEBUG_LEVEL_TRACE);
								DebugLibrary(this.getID() + " our action is 'nothing' so we're exiting here!", g_DEBUG_LEVEL_TRACE);
								action = 'load';
							} else {
								DebugLibrary(this.getID() + " Open: Our path did NOT contain 'pandora', 'sirius' or 'soundmachine' so we'll 'add'", g_DEBUG_LEVEL_TRACE);
								action = 'add';
							}
						} else {
							DebugLibrary(this.getID() + " Open: Our path did NOT start with '/radio', '/favorite' or '/service' so we'll 'add'", g_DEBUG_LEVEL_TRACE);
							action = 'add';
						}
					} else {
				 		DebugLibrary(this.getID() + " Open: Our action is '" + action + "' so we're gonna just do it!", g_DEBUG_LEVEL_TRACE);
					}

					queue.PlayThis(entry.path, action);
					return;
				} else {
					DebugLibrary(this.getID() + " Open: '" + entry.path + "' should be played, but we can't find the queue", g_DEBUG_LEVEL_WARN);
            	}
            	return; // We didn't have something that could be opened... so we're done here!
	 		}

			DebugLibrary(this.getID() + " Open: '" + entry.path + "' could not be opened, and could not be played... why did someone even try?", g_DEBUG_LEVEL_WARN);
 			return;
 		}
 		// If we get here we're just opening the location!!!
//System.Print("5");
 		this.recordTarget(entry);
//System.Print("6");
		this.OpenPath(entry.path, search);
//System.Print("7");
	} else {
		DebugLibrary(this.getID() + " can't open a non-existant index (index = " + index + ")", g_DEBUG_LEVEL_WARN);
//System.Print("A");

		if (loc.entries.length == 0) {
			DebugLibrary(this.getID() + " has a FUBAR location: " + loc.toString(), g_DEBUG_LEVEL_ERROR);
		}
	}
}

LibraryBrowser.prototype.OpenPath = function(path, search)
{
	if (path == undefined) {
		DebugLibrary(this.getID() + " can't open to an unspecified path... assuming /.", g_DEBUG_LEVEL_WARN);
		path = '/';
	}
	if (search === undefined) search = "";

	if (this.getBusy()) {
		DebugLibrary(this.getID() + " cannot navigate 'OpenPath(" + path + ")' as it's currenty busy.", g_DEBUG_LEVEL_WARN);
		return;
	}

	// This simply needs to create a new location and navigate to it, appending it on to the browser history!
	// We do NOT set an "openedIndex" on the current location because we didn't "descend" from ANYTHING!!!

	DebugLibrary(this.getID() + " opening to '" + path + "' [level = " + this.getCurrentLevel() + "]", g_DEBUG_LEVEL_INFO);

	if ((path == "/local/search") && (search == "")) {
		DebugLibrary(this.getID() + " '/local/path' must be searched, but we don't have a search term yet...", g_DEBUG_LEVEL_INFO);
		getKeyboard_fromViewID_andOpen(this._viewID, "", 'Library_OnSearchComplete(' + this._viewID + ', "' + path + '", "$1")');
		return;
	}



	// Create a new location with this parent and it's player MAC...
	var loc = new LibraryLocation(this, this._playerMac);

	// Add the location to our array (it'll be the current location now!)
	this.locations.push(loc);
// DEBUG We are changing the location list here

	// Start the fetch process for the location
	this.setBusy(true);
	loc.Fetch(path, search);

	// How do we handle searching?

}
/******************************************************************
 * END   Library Browser Object
 ******************************************************************/


/******************************************************************
 * START Library Manager Object (managing all browsers)
 ******************************************************************/
function LibraryManager(owningServer)
{
	this.Initialize();
	this._owner = owningServer; // The Server that owns this! Not used yet.
}

LibraryManager.prototype.Initialize = function()
{
	// We need an array of Library Browsers... one for each RTI System View.
	// Further, they need to know the view to which they are attached!
	this.browsers = new Array();
}

LibraryManager.prototype.toString = function()
{
	return "Manager has " + this.browsers.length + " browsers";
}


LibraryManager.prototype.GetBrowser_fromViewID = function(viewID)
{
	DebugLibrary("Manager: We need a browser for RTI System View '" + viewID + "'", g_DEBUG_LEVEL_TRACE);
	for (var x = 0; x < this.browsers.length; x++) 
	{
		if (this.browsers[x]._viewID == viewID) {
			DebugLibrary("Manager: Returning browser for RTI System View '" + viewID + "'", g_DEBUG_LEVEL_TRACE);
			return this.browsers[x];
		}
	}

	// If we get here we didn't have a browser for the view so we should create one!
	DebugLibrary("Manager: Creating browser for RTI System View '" + viewID + "'", g_DEBUG_LEVEL_TRACE);
	var browser = new LibraryBrowser(viewID, g_SERVER.GetAnyPlayerMAC());
	this.browsers.push(browser);
	browser.Home(); // Let's start off somewhere?
	return browser;
}

LibraryManager.prototype.MakeDirty = function(pathStartsWith)
{
	DebugLibrary("Make Dirty: Path starting with '" + pathStartsWith + "'", g_DEBUG_LEVEL_TRACE);

	// We go through ALL browsers and each of their locations and mark ANY where the path starts with /local/favorite as dirty 'makeDirty()'
	for (var x = 0; x < this.browsers.length; x++) {
		var browser = this.browsers[x];
			DebugLibrary("Make Dirty: Looking at " + browser.toString(), g_DEBUG_LEVEL_TRACE);

		for (var y = 0; y < browser.locations.length; y++) {
			location = browser.locations[y];
			DebugLibrary("Make Dirty: Looking at " + location.toString() + " to match '" + pathStartsWith + "'", g_DEBUG_LEVEL_TRACE);

			var path = location._fetch_path;
			if (path.startsWith(pathStartsWith)) {
				DebugLibrary("Make Dirty: " + location.toString() + " is DIRTY", g_DEBUG_LEVEL_INFO);
				location.makeDirty();
			} else {
				DebugLibrary("Make Dirty: " + location.toString() + " can be ignored", g_DEBUG_LEVEL_TRACE);
			}
		}
	}
}

LibraryManager.prototype.MakeDirty_Playlists = function(playlistId)
{
	var match = '/local/playlist';
	if ((playlistId === undefined) || (playlistId == null)) {
		// We match ALL playlist locations and sub-locations!
	} else {
		match = '/local/playlist=' + playlistId;
	}
	DebugLibrary("Make Dirty: Locations starting with " + match, g_DEBUG_LEVEL_TRACE);
	this.MakeDirty(match);
}

LibraryManager.prototype.MakeDirty_Favorites = function(pathStartsWith)
{
	var match = '/favorite';
	DebugLibrary("Make Dirty: Favorites starting with " + match, g_DEBUG_LEVEL_TRACE);
	this.MakeDirty(match);
}
/******************************************************************
 * END   Library Manager Object (managing all browsers)
 ******************************************************************/
