unit MServClients;
// (C) by Marco Schulze <marco@nightlabs.com>
// [YMD] 2001-03-07
// published under GPL (GNU Public Licence)

{$WARN UNIT_PLATFORM OFF}

interface
uses Windows, SysUtils, scktcomp, ExtCtrls, FileCtrl, MServCDatas, Defs;

type
  TSocketType = (stCmd, stRet);
  TRetCtrlStatus = (rcsWaitForData,rcsDataArrived);
  TQueueChangeType = (qctAdd, qctDelete);

type
  TMSrvHost = record
    Name : String;
    Port : Integer;
  end;
  TMSrvUser = record
    Name,
    Pwd   : String;
  end;

  TMServClient = class(TObject)
    constructor Create(); overload;
    constructor Create(const HostName:String;Port:Word); overload;
    destructor Destroy(); override;

    function ReadCompleteServerAnswerCmd():String;
    procedure CommandExec(Socket:TSocketType;Cmd:String;var ResI:Integer;var ResS:String);

    procedure Connect(const HostName:String;Port:Word);
    procedure Disconnect();
    function Login(UserName, Password : String;var SrvCode:Integer;var SrvMsg:String):Boolean;

    procedure CacheAlbums;
	 procedure CacheTracks(AlbumID:Integer);
	 procedure Enqueue(AlbumID,TrackID:Integer);
    procedure Unqueue(AlbumID,TrackID:Integer);
    procedure CacheQueue;
    function Play(var ResI:Integer;var ResS:String):Boolean;
    function Pause(var ResI:Integer;var ResS:String):Boolean;
    function Stop(var ResI:Integer;var ResS:String):Boolean;
    function Next(var ResI:Integer;var ResS:String):Boolean;
    function Repeat_(var ResI:Integer;var ResS:String):Boolean;
    function Rate(AlbumID,TrackID:Integer;Rating:TMServRating;var ResI:Integer;var ResS:String):Boolean;
    procedure RenameAlbum(AlbumID:Integer;const AlbumAuthor, AlbumName:String);
    procedure RenameTrack(AlbumID,TrackID:Integer;const TrackAuthor, TrackName, Genre:String);
    procedure ReadStatus;
    function ReadRatings(AlbumID, TrackID:Integer):TMServRatings;
    function Reset(var ResI:Integer;var ResS:String):Boolean;
    procedure SearchAlbums(const SearchStr:String);
	 procedure SearchTracks(const SearchStr:String);
	 procedure GetCover(AlbumID:Integer; var Coverdata: String);
	 procedure GetGenres(var saGenres: TStringArray);	 


	 function GetRandomSetting: Boolean;
	 procedure SetRandomSetting(RandomOn:Boolean);
	 procedure SetFilter(FilterOn:Boolean; FilterArg:String);

    procedure EnqueueTopX(Count:Integer);
    procedure ChangePassword(const OldPW, NewPW:String);
  protected
    MSCTimer                    : TTimer;
	 DoReloadQueue               : Boolean;
	 // use 2 sockets: one for commands, and one for information from the server
	 MServCSocketCmd,
    MServCSocketRet             : TClientSocket;
	 MServCSSCmd                 : TWinSocketStream;
    _Host                       : TMSrvHost;
    _User                       : TMSrvUser;

    RetCtrl                     : record
                                    Status  : TRetCtrlStatus;
                                    StartDT : TDateTime;
                                    RcvData : String;
                                    LastRcv : String;
                                  end;

    procedure MSCTimerOnTimer(Sendet:TObject);

    procedure OnRetDataRead(Sender: TObject; Socket: TCustomWinSocket);
  private
    function ExtractFirstLine(var OrgStr:String):String;
	 procedure ExtractFieldsFromLine(Line:String;var Fields:TStringArray);
  public
    MServCData                  : TMServCData;
    OnQueueChange               : procedure () of object;
    OnAlbumChange               : procedure (AlbumID:Integer) of object;
    OnTrackChange               : procedure (AlbumID,TrackID:Integer) of object;
    OnStatusChange              : procedure () of object;
    OnReset                     : procedure () of object;

    property User : TMSrvUser read _User;
    property Host : TMSrvHost read _Host;
  end;

const
  CmdSocket = stCmd;

procedure UUdecodeLine(UUin: string; var UUout: string);

var
  MServClient : TMServClient;

implementation

uses Forms;

var
	dummy1: integer;

const
  SocketGeneralTimeout = 60000;
  SocketSendTimeout = 90000;
  SocketReadTimeout = 90000;

  DataEndSeqCmd = ^m^j+'.'+^m^j;
  DataEndSeqRet = ^m^j;

constructor TMServClient.Create();
var
  i             : Integer;
  Buf           : Array[0..255] of Char;
  TempDir,s     : String;
begin
	inherited Create();

	i:=GetEnvironmentVariable('TEMP',Buf,SizeOf(Buf)-1);
	Buf[i]:=#0;
	TempDir:=StrPas(Buf);
	if Copy(TempDir,Length(TempDir),1)<>'\' then TempDir:=TempDir+'\';
	if not DirectoryExists(TempDir) then raise Exception.Create('Temp-directory does not exist!');

	i:=-1;
	repeat
		Inc(i);
		s:=TempDir+'MSCBuf'+IntToStr(i)+'.dsf';
	until not FileExists(s);

	// Create the COMMAND-socket, blocking
	MServCSocketCmd:=TClientSocket.Create(nil);
	MServCSocketCmd.ClientType:=ctBlocking;
	MServCSSCmd:=nil;
	MServCSSCmd:=TWinSocketStream.Create(MServCSocketCmd.Socket,SocketGeneralTimeout);

	// Create the INFO-socket, nonblocking. This one gets server messages
	MServCSocketRet:=TClientSocket.Create(nil);
	MServCSocketRet.ClientType:=ctNonBlocking;
	MServCSocketRet.OnRead:=OnRetDataRead;

	MServCData:=TMServCData.Create;

	OnQueueChange:=nil;
	OnAlbumChange:=nil;
	OnTrackChange:=nil;
	OnStatusChange:=nil;
	OnReset:=nil;

	MSCTimer:=TTimer.Create(nil);
	MSCTimer.OnTimer:=MSCTimerOnTimer;
	MSCTimer.Interval:=1000;
	MSCTimer.Tag:=0;
	MSCTimer.Enabled:=True;
end;

//=======================================================================
//	Constructor / destructor
//=======================================================================
constructor TMServClient.Create(const HostName:String;Port:Word);
begin
  Create();

  Connect(HostName, Port);
end;

destructor TMServClient.Destroy();
begin
  try
    if Assigned(MServCSocketCmd) then
      MServCSocketCmd.Close();

    if Assigned(MServCSocketRet) then
      MServCSocketRet.Close();

    if Assigned(MServCSocketCmd) then begin
      MServCSocketCmd.Free;
      MServCSocketCmd:=nil;
    end;

    if Assigned(MServCSSCmd) then begin
      MServCSSCmd.Free;
      MServCSSCmd:=nil;
    end;

    if Assigned(MServCSocketRet) then begin
      MServCSocketRet.Free;
      MServCSocketRet:=nil;
    end;

    if Assigned(MServCData) then begin
      MServCData.Free;
      MServCData:=nil;
    end;

    if Assigned(MSCTimer) then begin
      MSCTimer.Free;
      MSCTimer:=nil;
    end;

    inherited Destroy;
  except
    on E:Exception do raise Exception.Create('TMServClient.Destroy: '+E.Message);
  end;
end;


//=======================================================================
//  Connect - - both the cmd and the recv socket
//
// 	make sure a 200 is received from both
//=======================================================================

procedure TMServClient.Connect(const HostName:String;Port:Word);
var
  ResS         : String;
  StartDT      : TDateTime;
begin
  try
    if MServCSocketCmd.Active then MServCSocketCmd.Close;
    if MServCSocketRet.Active then MServCSocketRet.Close;

    _Host.Name:=HostName;
    _Host.Port:=Port;

    MServCSocketCmd.Host:=HostName;
    MServCSocketCmd.Port:=Port;

    MServCSocketRet.Host:=HostName;
    MServCSocketRet.Port:=Port;

    MServCSocketCmd.Open;
    ResS:=ReadCompleteServerAnswerCmd;
    if Copy(ResS,1,3)<>'200' then raise Exception.Create('Unexpected welcome message in SocketCmd: '+ResS);


    RetCtrl.Status:=rcsWaitForData;
    RetCtrl.RcvData:='';

    MServCSocketRet.Open;
    StartDT:=Now;
    repeat
      Application.ProcessMessages;
    until MServCSocketRet.Active or ((Now-StartDT)*24*3600>SocketGeneralTimeout);
    if not MServCSocketRet.Active then raise Exception.Create('Connection (SocketCmd) failed. Timeout(SocketGeneralTimeout)');

    RetCtrl.StartDT:=Now;
    repeat
      Application.ProcessMessages;
    until (RetCtrl.Status=rcsDataArrived) or ((Now-RetCtrl.StartDT)*24*3600>SocketReadTimeout);
    if (RetCtrl.Status<>rcsDataArrived) then raise Exception.Create('Read of welcome message (SocketRet) failed. Timeout(SocketReadTimeout)');

    ResS:=RetCtrl.LastRcv;
    if Copy(ResS,1,3)<>'200' then raise Exception.Create('Unexpected welcome message in SocketRet: '+ResS);

  except
    on E:Exception do raise Exception.Create('TMServClient.Connect: '+E.Message);
  end;
end;


//=======================================================================
//  OnRetDataRead - process info from recv-socket
//
//=======================================================================
procedure TMServClient.OnRetDataRead(Sender: TObject; Socket: TCustomWinSocket);
var
  SrvCode,i               : Integer;
  SrvMsg                  : String;
  Fields                  : TStringArray;
  _AlbumID,_TrackID       : Integer;
  _UserName               : String;
  Idx                     : Integer;
begin
	RetCtrl.StartDT:=Now;
	RetCtrl.RcvData:=RetCtrl.RcvData+Socket.ReceiveText;

	if RetCtrl.Status=rcsWaitForData then begin
		if Pos(DataEndSeqCmd,RetCtrl.RcvData)>0 then begin     // dataendseqcmd = end of command = crlf DOT crlf
			i:=Pos(DataEndSeqCmd,RetCtrl.RcvData);
			SrvMsg:=Copy(RetCtrl.RcvData,1,i+Length(DataEndSeqCmd)-1);
			Delete(RetCtrl.RcvData,1,i+Length(DataEndSeqCmd)-1);
			RetCtrl.LastRcv:=SrvMsg;
			RetCtrl.Status:=rcsDataArrived;
		end;
	end
	else begin
		// if RetCtrl.Status<>rcsWaitForData then begin

		// while a full line is still available
		while Pos(DataEndSeqRet,RetCtrl.RcvData)>0 do begin   // dataendseqret = CRLF
			// we've got a full line
			i:=Pos(DataEndSeqRet,RetCtrl.RcvData);
			SrvMsg:=Copy(RetCtrl.RcvData,1,i-1);        // chomp
			Delete(RetCtrl.RcvData,1,i+Length(DataEndSeqRet)-1); // chomp

			// we're only interested in info lines (starting with '=')
			if Copy(SrvMsg,1,1)='=' then begin

				SrvCode:=StrToInt(Copy(SrvMsg,2,3));	// srvcode is an info-number
				// remove =xxx from the beginning
				Delete(SrvMsg,1,4);

				// #9 löschen   remove tab
				Delete(SrvMsg,1,1);

				ExtractFieldsFromLine(SrvMsg,Fields);
				case SrvCode of
				600: 	begin // ein user hat sich eingeloggt   CONNECT
						end;
				601: 	begin // ein user hat sich ausgeloggt   DISCONN
							DoReloadQueue:=True;
						end;
				605: 	begin // Wiederbeginn Play nach Pause  	RESUME  Play of '%05' resumed by %01
							// Params: UserName AlbumID TrackID
							MServCData.SetStatus(
								MServCData.Status.NumberOfTracksIncluded,
								MServCData.Status.NumberOfTracksExcluded,
								msaPlaying,
								StrToInt(Fields[1]),
								StrToInt(Fields[2]),
								MServCData.Status.Position,
								Now,
								MServCData.Status.Ratings
							);
							if Assigned(OnStatusChange) then
								OnStatusChange;
						end;
				606: 	begin // pause    							PAUSE  Play of '%05' paused by %01
							MServCData.SetStatus(
								MServCData.Status.NumberOfTracksIncluded,
								MServCData.Status.NumberOfTracksExcluded,
								msaPaused,
								MServCData.Status.AlbumID,
								MServCData.Status.TrackID,
								Trunc(MServCData.Status.Position+(Now-MServCData.Status.FetchPosDT)* 24 * 3600),
								Now,
								MServCData.Status.Ratings
							);
							if Assigned(OnStatusChange) then
								OnStatusChange;
						end;
				607: 	begin // playing finished (no more track in queue)   FINISH
							MServCData.SetStatus(
								MServCData.Status.NumberOfTracksIncluded,
								MServCData.Status.NumberOfTracksExcluded,
								msaNothingPlaying,
								-1, // AlbumID
								-1, // TrackID
								0, // MServCData.Status.Position
								Now,
								EmptyMServRatings
							);
							if Assigned(OnStatusChange) then
								OnStatusChange;
						end;
				608: 	begin // repeat         				REPEAT The current track has been put back in the queue by %01
							DoReloadQueue:=True;
						end;
				609,       	// Set Author     				AUTH
				610: 	begin	// set name							NAME
							// Params: UserName AlbumID TrackID TrackAuthor TrackName setvalue
							//
							_AlbumID:=StrToInt(Fields[1]);
							_TrackID:=StrToInt(Fields[2]);

							MServCData.Albums[MServCData.GetAlbumIdx(_AlbumID)].SetTrack(
								_AlbumID,
								_TrackID,
								Fields[3],
								Fields[4],
								'',	// genre
								msrUndefined,
								-1
							);
							if Assigned(OnTrackChange) then
								OnTrackChange(_AlbumID,_TrackID);
						end;

				612: 	begin // Set Genre   					GENRE
							// Params: UserName AlbumID TrackID TrackAuthor TrackName Genre
							//
							_AlbumID:=StrToInt(Fields[1]);
							_TrackID:=StrToInt(Fields[2]);

							MServCData.Albums[MServCData.GetAlbumIdx(_AlbumID)].SetTrack(
								_AlbumID,
								_TrackID,
								'',
								'',
								Fields[5],	// genre
								msrUndefined,
								-1
							);
							if Assigned(OnTrackChange) then
								OnTrackChange(_AlbumID,_TrackID);
						end;
				614,        //									RATE     The track %02/%03 has been rated %06 by %01
				615: 	begin // RATE: es wurde gerated	RATECUR  This track has been rated %06 by %01
							// Params: UserName AlbumID TrackID TrackAuthor TrackName Rating

							_UserName:=Fields[0];
							_AlbumID:=StrToInt(Fields[1]);
							_TrackID:=StrToInt(Fields[2]);

							// eigenes Rating nur setzen, wenn UserName der aktuelle User ist!
							if AnsiLowerCase(Fields[0])=AnsiLowerCase(User.Name) then begin
								MServCData.Albums[MServCData.GetAlbumIdx(_AlbumID)].SetTrack(
									_AlbumID,
									_TrackID,
									'',
									'',
									'',	// genre
									MServRatingStringToMServRating(Fields[5]),
									-1
								);
								if Assigned(OnTrackChange) then
									OnTrackChange(_AlbumID,_TrackID);
							end; // if AnsiLowerCase(Fields[0])=AnsiLowerCase(User.Name) then begin

							// Status.AlleRatings setzen, wenn aktueller Track gerated wurde
							if (_AlbumID = MServCData.Status.AlbumID) and
								(_TrackID = MServCData.Status.TrackID) then begin
								// ist der ratende User schon in der Liste?
								Idx:=-1;
								for i:=0 to High(MServCData.Status.Ratings) do begin
									if AnsiLowerCase(MServCData.Status.Ratings[i].UserName)=AnsiLowerCase(_UserName) then begin
										Idx:=i;
										Break;
									end;
								end; // for i:=0 to High(MServCData.Status.Ratings) do begin
								if Idx<0 then begin
									SetLength(MServCData.Status.Ratings,High(MServCData.Status.Ratings)+2);
									Idx:=High(MServCData.Status.Ratings);
									MServCData.Status.Ratings[Idx].UserName:=_UserName;
								end;

								MServCData.Status.Ratings[Idx].Rating:=MServRatingStringToMServRating(Fields[5]);
							end; // wurde der aktuelle Track gerated?

							if Assigned(OnStatusChange) then
								OnStatusChange();
						end;

				619:	begin // Track wurde eingequed          QADDTR
							// Params: UserName AlbumID TrackID TrackAuthor TrackName Rating TrackTime
							DoReloadQueue:=True;
						end;
				622: 	begin // Play           					NOWPLAY
							// Params: UserName AlbumID TrackID TrackAuthor TrackName Rating TrackTime
							_AlbumID:=StrToInt(Fields[1]);
							_TrackID:=StrToInt(Fields[2]);

							MServCData.SetStatus(
								MServCData.Status.NumberOfTracksIncluded,
								MServCData.Status.NumberOfTracksExcluded,
								msaPlaying,
								_AlbumID,
								_TrackID,
								0, // MServCData.Status.Position
								Now,
								ReadRatings(_AlbumID, _TrackID)
							);

							MServCData.DeleteFromQueue(
								_AlbumID,
								_TrackID
							);

							if Assigned(OnQueueChange) then
								OnQueueChange;
							if Assigned(OnStatusChange) then
								OnStatusChange;
						end;
				623: 	begin // Stop     							STOPPED
							// Params: UserName AlbumID TrackID TrackAuthor TrackName
							MServCData.SetStatus(
								MServCData.Status.NumberOfTracksIncluded,
								MServCData.Status.NumberOfTracksExcluded,
								msaNothingPlaying,
								-1, // AlbumID
								-1, // TrackID
								0, // MServCData.Status.Position
								Now,
								EmptyMServRatings
							);

							if Assigned(OnStatusChange) then
								OnStatusChange;
						end;
				627: 	begin // unqueue   							UNQ
							// Params: UserName AlbumID TrackID TrackAuthor TrackName
							MServCData.DeleteFromQueue(
								StrToInt(Fields[1]),
								StrToInt(Fields[2])
							);
							if Assigned(OnQueueChange) then
								OnQueueChange;
				end;
				629: 	begin // Next   								SKIP	629	Track skipped by %01
							// Params: UserName
							// nach 629 kommt auch 622
						end;
				630:	begin // Reset   								RESET	630	Server has been cleared and reset by %01
							if Assigned(OnReset) then
								OnReset;
						end;
				634,       // Set AlbumAuthor
				635: 	begin // Set AlbumName
							// Params: UserName AlbumID AlbumAuthor AlbumName {???}
							MServCData.SetAlbum(
								StrToInt(Fields[1]),
								Fields[2],
								Fields[3]
							);

							if Assigned(OnAlbumChange) then
								OnAlbumChange(StrToInt(Fields[1]));
						end;
				end; // case SrvCode of       
			end; // if Copy(RetCtrl.RcvData,1,1)='=' then begin
		end; // while Pos(DataEndSeqRet,RetCtrl.RcvData)>0 do begin
	end; // if RetCtrl.Status<>rcsWaitForData then begin
end;

//=======================================================================
//	Disconnect from server
//=======================================================================
procedure TMServClient.Disconnect();
begin
  try
	 if MServCSocketCmd.Active then MServCSocketCmd.Close;
	 _Host.Name:='';
	 _Host.Port:=0;
	 _User.Name:='';
	 _User.Pwd:='';

  except
    on E:Exception do raise Exception.Create('TMServClient.Disconnect: '+E.Message);
  end;
end;

//=======================================================================
//	ReadCompleteServerAnswerCmd
//		read everyting up to the dataend-marker (a DOT)
//=======================================================================
function TMServClient.ReadCompleteServerAnswerCmd():String;
var
	RcvBuf            : String;
	BytesRead         : Integer;
	DataReceiveDone   : Boolean;
	const
	RcvBufSize = 255;
begin
	Result:='';
	try
		repeat
			SetLength(RcvBuf,RcvBufSize);
			if not MServCSSCmd.WaitForData(SocketReadTimeout) then
				raise Exception.Create('Read (SocketCmd) failed. Timeout(ReadTimeout)!');
			BytesRead:=MServCSSCmd.Read(RcvBuf[1],Length(RcvBuf)-1);
			SetLength(RcvBuf,BytesRead);
			Result:=Result+RcvBuf;
			DataReceiveDone:=Pos(DataEndSeqCmd,Result)>0;   // dataendseqcmd = end of command = crlf DOT crlf
		until DataReceiveDone;
	except
		on E:Exception do
			raise Exception.Create('TMServClient.ReadCompleteServerAnswerCmd: '+E.Message);
	end;
end;

//=======================================================================
//	Commandexec
//		Send a command and read everything back. until the end
//		ResI containes the result-number
//		ResS containes the resulting data (lines)
//=======================================================================
procedure TMServClient.CommandExec(Socket:TSocketType;Cmd:String;var ResI:Integer;var ResS:String);
var
	RcvString         : String;
	i                 : Integer;
begin
	ResI:=-1;
	ResS:='ERROR'+#3+'Internal.CommandExec.Unknown';
	try
		Cmd:=Cmd+^m;

		if Socket=stCmd then begin
			// for the cmd-socket its easy:
			//		write and read a complete answer back
			MServCSSCmd.WriteBuffer(Cmd[1],Length(Cmd));
			if not MServCSSCmd.WaitForData(SocketSendTimeout) then
				raise Exception.Create('Write (SocketCmd) failed. Timeout(SendTimeout)!');
			RcvString:=ReadCompleteServerAnswerCmd();
		end // if Socket=stCmd then begin
		else begin
			// if Socket=stRet then begin

			RetCtrl.Status:=rcsWaitForData;
			MServCSocketRet.Socket.SendText(Cmd);
			RetCtrl.StartDT:=Now;
			repeat
				Application.ProcessMessages;
			until (RetCtrl.Status=rcsDataArrived) or
					((Now-RetCtrl.StartDT)*24*3600>SocketReadTimeout);
			if (RetCtrl.Status<>rcsDataArrived) then
				raise Exception.Create('Read (SocketRet) failed. Timeout(SocketReadTimeout)!');

			RcvString:=RetCtrl.LastRcv;
		end;

		// Now we have a result. Should begin with a number
		try
			i:=StrToInt(Copy(RcvString,1,3));
		except
			on E:Exception do begin
				ResI:=-2;
				ResS:='CommandExec: Invalid server-answer: '+RcvString;
				Exit;
			end;
		end;

		ResI:=i;
		// remove number from stringresult
		Delete(RcvString,1,3);
		// remove leading spaces
		if Copy(RcvString,1,1)=' ' then
			Delete(RcvString,1,1);
		// chomp the endseq marker
		Delete(RcvString,Pos(DataEndSeqCmd,RcvString),Length(DataEndSeqCmd));
		ResS:=RcvString;
	except
		on E:Exception do
			raise Exception.Create('TMServClient.CommandExec: '+E.Message);
	end;
end;

//=======================================================================
//	Login - send user and password for BOTH cmd and recv sockets
//=======================================================================
function TMServClient.Login(UserName, Password : String;var SrvCode:Integer;var SrvMsg:String):Boolean;
var
  ResI        : Integer;
  ResS        : String;
  Socket      : TSocketType;
begin
	try
		Result:=False;
		SrvCode:=-1;
		SrvMsg:='';
		_User.Name:=UserName;
		_User.Pwd:=Password;

		if Password='' then begin
			SrvCode:=-1;
			SrvMsg:='Password is empty!';
			Exit;
		end;
		//
		// open 2 sockets to the server! (one not and one realtime)
		for Socket:=stCmd to stRet do begin
			CommandExec(Socket,'user '+UserName,ResI,ResS);
			// 201 is USEROK
			if ResI<>201 then begin
				SrvCode:=ResI;
				SrvMsg:=ResS;
				Exit;
			end;
			// RTCOMPUTER = real-time. This gives unrequested info-messages (started with '=')
			//
			if Socket=stRet then begin
				CommandExec(Socket,'pass '+Password+' RTCOMPUTER',ResI,ResS);
			end
			else begin
				CommandExec(Socket,'pass '+Password+' COMPUTER',ResI,ResS);
			end;
			// 202 is ACLSUCC  (authorisation ok)
			if ResI<>202 then begin
				SrvCode:=ResI;
				SrvMsg:=ResS;
				Exit;
			end;

		end; // for Socket:=Low(TSocketType) to High(TSocketType) do begin

		Result:=True;
	except
	on E:Exception do
		raise Exception.Create('TMServClient.Login: '+E.Message);
	end;
end;

//=======================================================================
// ExtractFirstLine     (should be called ExtractNextLine)
// return the first line of Orgstr and remove this line from OrgStr
//=======================================================================
function TMServClient.ExtractFirstLine(var OrgStr:String):String;
var
  i          : Integer;
const
  LineSep = ^m^j;    // CR-LF separator
begin
  Result:='';
  i:=Pos(LineSep,OrgStr);
  if i<1 then i:=Length(OrgStr)+1;
  Result:=Copy(OrgStr,1,i-1);
  Delete(OrgStr,1,i-1+Length(LineSep));
end;

//=======================================================================
// ExtractFieldsFromLine - split line up into tab-separated fields.
// Return fieldsarray
//=======================================================================
procedure TMServClient.ExtractFieldsFromLine(Line:String;var Fields:TStringArray);
var
  i        : Integer;
  s        : String;
const
  FieldSep = #9; 	// tab used as separator
begin
  SetLength(Fields,0);
  while Line<>'' do begin
    i:=Pos(FieldSep,Line);
    if i<1 then i:=Length(Line)+1;
    s:=Copy(Line,1,i-1);
    Delete(Line,1,i-1+Length(FieldSep));

	 SetLength(Fields,High(Fields)+2);  // high gives nrvalues-1, so 0 values gives -1+2 = 1 for the first
	 Fields[High(Fields)]:=s;
  end; // while Line<>'' do begin

end;


//=======================================================================
//		HIGHER LEVEL COMMANDS
//=======================================================================
//=======================================================================
//=======================================================================
// Search albums using ASEARCH
//=======================================================================
procedure TMServClient.SearchAlbums(const SearchStr:String);
var
  ResI                       : Integer;
  ResS                       : String;
  s                          : String;
  Fields                     : TStringArray;
  AlbumID                    : Integer;
begin
	try
		MServCData.ClearFoundAlbums;

		CommandExec(CmdSocket,'asearch '+SearchStr,ResI,ResS);
		//	  280 - found some (followed by albums found)
		//		538 - found nada
		if ResI=538 then
			Exit; // nix gefunden...
		if ResI<>280 then
			raise Exception.Create('"asearch" failed: ['+IntToStr(ResI)+'] '+ResS);

		// first line contains "search results" - can be missed
		ExtractFirstLine(ResS);

		// decode the rest.
		// It contains Lines with: AlbumID Artist Name
		while ResS<>'' do begin
			s:=ExtractFirstLine(ResS);
			ExtractFieldsFromLine(s,Fields);

			AlbumID:=StrToInt(Fields[0]);

			if MServCData.GetAlbumIdx(AlbumID)<0 then
				raise Exception.Create('No album with AlbumID='+IntToStr(AlbumID)+' cached!');

			MServCData.AddToFoundAlbums(AlbumID);
		end; //while ResS<>'' do begin

	except
		on E:Exception do
			raise Exception.Create('TMServClient.SearchAlbums: '+E.Message);
	end;
end;

//=======================================================================
// Search tracks using SEARCH
//=======================================================================
procedure TMServClient.SearchTracks(const SearchStr:String);
var
  ResI                       : Integer;
  ResS                       : String;
  s                          : String;
  Fields                     : TStringArray;
  AlbumID, TrackID           : Integer;
  AlbumIdx                   : Integer;
begin
	try
		MServCData.ClearFoundTracks;

		CommandExec(CmdSocket,'search '+SearchStr,ResI,ResS);
		if ResI=520 then
			Exit; // nix gefunden...
		if ResI<>243 then
			raise Exception.Create('"search" failed: ['+IntToStr(ResI)+'] '+ResS);

		// first line contains "search results" - can be missed
		ExtractFirstLine(ResS);

		// jetzt die Zeilen auswerten
		while ResS<>'' do begin
			s:=ExtractFirstLine(ResS);
			ExtractFieldsFromLine(s,Fields);

			AlbumID:=StrToInt(Fields[0]);
			TrackID:=StrToInt(Fields[1]);

			AlbumIdx:=MServCData.GetAlbumIdx(AlbumID);

			if AlbumIdx<0 then
				raise Exception.Create('No album with AlbumID='+IntToStr(AlbumID)+' cached!');

			if MServCData.Albums[AlbumIdx].GetTrackIdx(TrackID)<0 then begin
				CacheTracks(AlbumID);
				if MServCData.Albums[AlbumIdx].GetTrackIdx(TrackID)<0 then
					raise Exception.Create('No track with AlbumID='+IntToStr(AlbumID)+' && TrackID='+IntToStr(TrackID)+' cached! It seems not to exist on the server!');
			end;
			MServCData.AddToFoundTracks(StrToInt(Fields[0]), StrToInt(Fields[1]));
		end; //while ResS<>'' do begin

	except
		on E:Exception do raise Exception.Create('SearchTracks: '+E.Message);
	end;
end;

//=======================================================================
// CacheAlbums - Issue the ALBUMS command and get a list of all albums
//=======================================================================
procedure TMServClient.CacheAlbums;
var
  ResI     : Integer;
  ResS     : String;
  s        : String;
  Fields   : TStringArray;
begin
	try
	MServCData.ClearAlbums;

	CommandExec(CmdSocket,'albums',ResI,ResS);
	if ResI<>227 then
		raise Exception.Create('"albums" failed: ['+IntToStr(ResI)+'] '+ResS);

	// erste Zeile kann man wegschmeißen...
	ExtractFirstLine(ResS);

	// jetzt die Zeilen auswerten
	while ResS<>'' do begin
		s:=ExtractFirstLine(ResS);
		ExtractFieldsFromLine(s,Fields);

		MServCData.SetAlbum(StrToInt(Fields[0]),Fields[1],Fields[2]);
	end; //while ResS<>'' do begin

	except
		on E:Exception do
			raise Exception.Create('TMServClient.CacheAlbums: '+E.Message);
	end;
end;

//=======================================================================
// CacheTracks - Issue the TRACKS command and get info on one album
//=======================================================================
procedure TMServClient.CacheTracks(AlbumID:Integer);
var
  AlbumIdx          : Integer;
  TrackID			  : Integer;
  ResI,ResI2        : Integer;
  TAuth,Tname			: string;
  TRate,TDuration		: string;
  TGenre					: string;
  ResS,ResS2        : String;
  s                 : String;
  Fields            : TStringArray;
begin
	try
		AlbumIdx:=MServCData.GetAlbumIdx(AlbumID);
		if AlbumIdx<0 then
			raise Exception.Create('No album with ID='+IntToStr(AlbumID)+' in cache!');

		MServCData.Albums[AlbumIdx].ClearTracks;

		CommandExec(CmdSocket,'tracks '+IntToStr(AlbumID),ResI,ResS);
		if ResI<>228 then
			raise Exception.Create('"tracks" failed: ['+IntToStr(ResI)+'] '+ResS);
		// first line: 'tracks in album' can be missed
		ExtractFirstLine(ResS);
		// second line: albuminformation. Can be missed
		ExtractFirstLine(ResS);

		// jetzt die Zeilen auswerten
		while ResS<>'' do begin
			s:=ExtractFirstLine(ResS);
			ExtractFieldsFromLine(s,Fields);
			//				0			1			2				3			4			5
			// Fields: albumid TrackID TrackAuthor TrackName Rating Duration
			TrackID:=StrToInt(Fields[1]);
			TAuth:=Fields[2];
			Tname:=Fields[3];
			TRate:=Fields[4];
			TDuration:=Fields[5];
			begin
				// now get extra track information
				CommandExec(CmdSocket,'info '+IntToStr(AlbumID)+' '+IntToStr(TrackID),ResI2,ResS2);
				if ResI2<>246 then
					raise Exception.Create('"info" failed: ['+IntToStr(ResI2)+'] '+ResS2);
				// first line: 'album xx track xx' can be missed
				ExtractFirstLine(ResS2);
				//INFT	246	Album %01, track %02
				//INFT1	246	  Track name:      %06
				//INFT2	246	        author:    %05
				//INFT3	246	  Album name:      %04
				//INFT4	246	        author:    %03
				//INFT5	246	  Dated:           %07
				//INFT6	246	  Duration:        %15 (%16)
				//INFT7	246	  Genre(s):        %13
				//INFT8	246	  Played:          %09
				//INFT9	246	  Filter:          %14
				//INFT10	246	  Rated:           %12; mean %10%%
				//INFT11	246	                   temporally adjusted to %11%%
				// jetzt die Zeilen auswerten
				while ResS2<>'' do begin
					s:=ExtractFirstLine(ResS2);
					ExtractFieldsFromLine(s,Fields);
					TGenre:=Fields[13-1];	// count from 0
				end;
			end;
			MServCData.Albums[AlbumIdx].SetTrack(
				AlbumID,
				TrackID,
				TAuth,
				TName,
				TGenre,	// genre
				MServRatingStringToMServRating(TRate),
				HourMinStrToSecs(TDuration)
			);
		end; //while ResS<>'' do begin

	except
		on E:Exception do
			raise Exception.Create('TMServClient.CacheTracks: '+E.Message);
	end;
end;



//=======================================================================
// GetCover - read base64 uuencoded CD-cover picture
//=======================================================================
procedure TMServClient.GetCover(AlbumID:Integer; var Coverdata: String);
var
  ResI     : Integer;
  ResS     : String;
  s,us        : String;
  Fields   : TStringArray;
  tot1,tot2: integer;
begin
	tot1:=0;
	tot2:=0;
	try
	CommandExec(CmdSocket,'x cover '+IntToStr(AlbumID),ResI,ResS);
	if ResI<>227 then
		raise Exception.Create('"x cover" failed: ['+IntToStr(ResI)+'] '+ResS);

	// erste Zeile kann man wegschmeißen...
	ExtractFirstLine(ResS);

	// jetzt die Zeilen auswerten
	while ResS<>'' do begin
		s:=ExtractFirstLine(ResS);
		// uuencoded..
		UUdecodeLine(s,us);
		Coverdata:= coverdata+us;
		tot1:=tot1+length(s);
		tot2:=tot2+length(us);
	end; //while ResS<>'' do begin
	dummy1:=tot1+tot2;
	except
		on E:Exception do
			raise Exception.Create('TMServClient.GetCover: '+E.Message);
	end;
end;


//=======================================================================
// GetGenres - read all existing Genres//
//=======================================================================
procedure TMServClient.GetGenres(var saGenres: TStringArray);
var
	ResI     :Integer;
	ResS     :String;
	s			:String;
	Fields   :TStringArray;
	sGenre	:String;
begin
	try
	CommandExec(CmdSocket,'x genres',ResI,ResS);
	if ResI<>271 then
		raise Exception.Create('"x genres" failed: ['+IntToStr(ResI)+'] '+ResS);

	// erste Zeile kann man wegschmeißen...
	ExtractFirstLine(ResS);
	SetLength(saGenres,0);
	// jetzt die Zeilen auswerten
	while ResS<>'' do begin
		s:=ExtractFirstLine(ResS);
		ExtractFieldsFromLine(s,Fields);
		sGenre:=Fields[1];	// count from 0
		if Length(sGenre)>0 then begin
			SetLength(saGenres,High(saGenres)+2);  // high gives nrvalues-1, so 0 values gives -1+2 = 1 for the first
			saGenres[High(saGenres)]:=sGenre;
		end;
	end; //while ResS<>'' do begin

	except
		on E:Exception do
			raise Exception.Create('TMServClient.GetGenres: '+E.Message);
	end;
end;

procedure TMServClient.Enqueue(AlbumID,TrackID:Integer);
// TrackID = -1, um ganzes Album zu queuen
var
  ResI           : Integer;
  ResS           : String;
  Param          : String;
begin
  try
    Param:=IntToStr(AlbumID);
    if TrackID>=0 then Param:=Param+' '+IntToStr(TrackID);
    CommandExec(CmdSocket,'queue '+Param,ResI, ResS);
	 //if ResI<>247 then
      //raise Exception.Create('"queue" failed: ['+IntToStr(ResI)+'] '+ResS);

  except
    on E:Exception do raise Exception.Create('TMServClient.Enqueue: '+E.Message);
  end;
end;

procedure TMServClient.Unqueue(AlbumID,TrackID:Integer);
var
  ResI           : Integer;
  ResS           : String;
  Param          : String;
begin
  try
    Param:=IntToStr(AlbumID)+' '+IntToStr(TrackID);
    CommandExec(CmdSocket,'unqueue '+Param,ResI, ResS);
    if ResI<>254 then raise Exception.Create('"unqueue" failed: ['+IntToStr(ResI)+'] '+ResS);

  except
    on E:Exception do raise Exception.Create('TMServClient.Unqueue: '+E.Message);
  end;
end;

function TMServClient.Rate(AlbumID,TrackID:Integer;Rating:TMServRating;var ResI:Integer;var ResS:String):Boolean;
// TrackID = -1, um ganzes Album zu raten
// und AlbumID = -1, um aktuellen Track zu raten
var
  Param          : String;
begin
  Result:=False;
  try
    Param:='';
    if AlbumID>=0 then Param:=Param+' '+IntToStr(AlbumID);
    if TrackID>=0 then Param:=Param+' '+IntToStr(TrackID);
    Param:=Param+' '+CMServRatingStr[Rating];

    CommandExec(CmdSocket,'rate'+Param,ResI, ResS);
    
    if ResI=270 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Enqueue: '+E.Message);
  end;
end;

procedure TMServClient.CacheQueue;
var
  ResI         : Integer;
  ResS         : String;
  s            : String;
  Fields       : TStringArray;
  CurrAlbumID  : Integer;
  CurrTrackID  : Integer;
begin
  try
    MServCData.ClearQueueTracks;

    CommandExec(CmdSocket,'queue',ResI,ResS);
    if ResI=404 then Exit; // there are no tracks in the queue
    if ResI<>225 then raise Exception.Create('"tracks" failed: ['+IntToStr(ResI)+'] '+ResS);

    ExtractFirstLine(ResS);
    // die erste Zeile kann man wegschmeißen...

    // jetzt die Zeilen auswerten
    while ResS<>'' do begin
      s:=ExtractFirstLine(ResS);
      ExtractFieldsFromLine(s,Fields);

      CurrAlbumID:=StrToInt(Fields[1]);
      CurrTrackID:=StrToInt(Fields[2]);

      CacheTracks(CurrAlbumID);

      MServCData.AddToQueue(
        CurrAlbumID,
        CurrTrackID,
        Fields[0]
      );
    end; //while ResS<>'' do begin

    DoReloadQueue:=False;

  except
    on E:Exception do raise Exception.Create('TMServClient.CacheQueue: '+E.Message);
  end;
end;

function TMServClient.Play(var ResI:Integer;var ResS:String):Boolean;
begin
  try
    Result:=False;
    CommandExec(CmdSocket,'play',ResI,ResS);
    if ResI=230 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Play: '+E.Message);
  end;
end;

function TMServClient.Pause(var ResI:Integer;var ResS:String):Boolean;
begin
  try
    Result:=False;
    CommandExec(CmdSocket,'pause',ResI,ResS);
    if ResI=232 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Pause: '+E.Message);
  end;
end;

function TMServClient.Stop(var ResI:Integer;var ResS:String):Boolean;
begin
  try
    Result:=False;
    CommandExec(CmdSocket,'stop',ResI,ResS);
    if ResI=231 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Stop: '+E.Message);
  end;
end;

function TMServClient.Next(var ResI:Integer;var ResS:String):Boolean;
begin
  try
    Result:=False;
    CommandExec(CmdSocket,'next',ResI,ResS);
    if ResI=240 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Next: '+E.Message);
  end;
end;

function TMServClient.Repeat_(var ResI:Integer;var ResS:String):Boolean;
begin
  try
    Result:=False;
    CommandExec(CmdSocket,'repeat',ResI,ResS);
    if ResI=608 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Repeat_: '+E.Message);
  end;
end;

function TMServClient.Reset(var ResI:Integer;var ResS:String):Boolean;
begin
  try
    Result:=False;
    CommandExec(CmdSocket,'reset',ResI,ResS);
    if ResI=258 then Result:=True;
  except
    on E:Exception do raise Exception.Create('TMServClient.Next: '+E.Message);
  end;
end;

procedure TMServClient.RenameAlbum(AlbumID:Integer;const AlbumAuthor, AlbumName:String);
var
  ResI : Integer;
  ResS : String;
begin
  try
    CommandExec(CmdSocket,'set albumauthor '+IntToStr(AlbumID)+' '+AlbumAuthor,ResI,ResS);
    if ResI<>260 then raise Exception.Create('"set albumauthor" failed: ['+IntToStr(ResI)+'] '+ResS);

    CommandExec(CmdSocket,'set albumname '+IntToStr(AlbumID)+' '+AlbumName,ResI,ResS);
    if ResI<>261 then raise Exception.Create('"set albumname" failed: ['+IntToStr(ResI)+'] '+ResS);

  except
    on E:Exception do raise Exception.Create('TMServClient.RenameAlbum: '+E.Message);
  end;
end;

procedure TMServClient.RenameTrack(AlbumID,TrackID:Integer;const TrackAuthor, TrackName, Genre:String);
var
  ResI : Integer;
  ResS : String;
begin
	try
		if TrackAuthor<>'' then begin
			CommandExec(CmdSocket,'set author '+IntToStr(AlbumID)+' '+IntToStr(TrackID)+' '+TrackAuthor,ResI,ResS);
			if ResI<>260 then
				raise Exception.Create('"set author" failed: ['+IntToStr(ResI)+'] '+ResS);
		end;

		if TrackName<>'' then begin
			CommandExec(CmdSocket,'set name '+IntToStr(AlbumID)+' '+IntToStr(TrackID)+' '+TrackName,ResI,ResS);
			if ResI<>261 then
				raise Exception.Create('"set name" failed: ['+IntToStr(ResI)+'] '+ResS);
		end;
		if Genre<>'' then begin
			CommandExec(CmdSocket,'set genre '+IntToStr(AlbumID)+' '+IntToStr(TrackID)+' '+Genre,ResI,ResS);
			if ResI<>263 then
				raise Exception.Create('"set genre" failed: ['+IntToStr(ResI)+'] '+ResS);
		end;
	except
		on E:Exception do raise Exception.Create('TMServClient.RenameAlbum: '+E.Message);
	end;
end;

procedure TMServClient.ReadStatus;
var
  ResI         : Integer;
  ResS         : String;
  Stat         : TMServStatus;
  Fields       : TStringArray;
begin
  try
    CommandExec(CmdSocket,'status',ResI,ResS);
    if ResI=224 then begin // nothing currently playing
      ExtractFirstLine(ResS);

      ExtractFieldsFromLine(ResS,Fields);

      Stat.NumberOfTracksIncluded:=StrToInt(Fields[1]);
      Stat.NumberOfTracksExcluded:=StrToInt(Fields[2]);

      Stat.Action:=msaNothingPlaying;
      Stat.AlbumID:=-1;
      Stat.TrackID:=-1;
      Stat.Position:=0;
      Stat.FetchPosDT:=Now;

      SetLength(Stat.Ratings,0);
    end
    else if ResI=222 then begin // playing
      ExtractFirstLine(ResS);

      ExtractFieldsFromLine(ResS,Fields);

      Stat.NumberOfTracksIncluded:=StrToInt(Fields[1]);
      Stat.NumberOfTracksExcluded:=StrToInt(Fields[2]);

      Stat.Action:=msaPlaying;
      Stat.AlbumID:=StrToInt(Fields[3]);
      Stat.TrackID:=StrToInt(Fields[4]);
      Stat.Position:=HourMinStrToSecs(Fields[8]);
      Stat.FetchPosDT:=Now;

      Stat.Ratings:=ReadRatings(Stat.AlbumID,Stat.TrackID);
    end
    else if ResI=223 then begin // paused
      ExtractFirstLine(ResS);

      ExtractFieldsFromLine(ResS,Fields);

      Stat.NumberOfTracksIncluded:=StrToInt(Fields[1]);
      Stat.NumberOfTracksExcluded:=StrToInt(Fields[2]);

      Stat.Action:=msaPaused;
      Stat.AlbumID:=StrToInt(Fields[3]);
      Stat.TrackID:=StrToInt(Fields[4]);
      Stat.Position:=HourMinStrToSecs(Fields[8]);
      Stat.FetchPosDT:=Now;

      Stat.Ratings:=ReadRatings(Stat.AlbumID,Stat.TrackID);
    end
    else raise Exception.Create('"status" failed: ['+IntToStr(ResI)+'] '+ResS);

    MServCData.SetStatus(
      Stat.NumberOfTracksIncluded,
      Stat.NumberOfTracksExcluded,
      Stat.Action,
      Stat.AlbumID,
      Stat.TrackID,
      Stat.Position,
      Stat.FetchPosDT,
      Stat.Ratings
    );

  except
    on E:Exception do raise Exception.Create('TMServClient.ReadStatus: '+E.Message);
  end;
end;

function TMServClient.ReadRatings(AlbumID, TrackID:Integer):TMServRatings;
var
  ResI         : Integer;
  ResS, s      : String;
  Fields       : TStringArray;
  Param        : String;
begin
  try
    if (AlbumID>=0) and (TrackID>=0) then
      Param:=' '+IntToStr(AlbumID)+' '+IntToStr(TrackID)
    else
      Param:='';

    CommandExec(CmdSocket,'ratings'+Param,ResI,ResS);
    if ResI<>229 then raise Exception.Create('"ratings" failed: ['+IntToStr(ResI)+'] '+ResS);

    // die ersten zwei Zeile wegschmeißen
    ExtractFirstLine(ResS);
    ExtractFirstLine(ResS);

    SetLength(Result,0);
    // jetzt die Zeilen auswerten
    while ResS<>'' do begin
      s:=ExtractFirstLine(ResS);
      ExtractFieldsFromLine(s,Fields);

      SetLength(Result,High(Result)+2);
      Result[High(Result)].UserName := Fields[0];
      Result[High(Result)].Rating   := MServRatingStringToMServRating(Fields[1]);

      if Result[High(Result)].Rating=msrUndefined then begin
        if (Fields[1]<>'HEARD') and (Fields[1]<>'UNHEARD') then
          raise Exception.Create('Unknown rating: "'+Fields[1]+'"');
      end; // if Result[High(Result)].Rating=msrUndefined then begin

    end; //while ResS<>'' do begin

  except
    on E:Exception do raise Exception.Create('TMServClient.ReadRatings: '+E.Message);
  end;
end;

procedure TMServClient.MSCTimerOnTimer(Sendet:TObject);
begin
  MSCTimer.Tag:=MSCTimer.Tag+1;
  if MSCTimer.Tag>=24 * 3600 then MSCTimer.Tag:=0;

  if MSCTimer.Tag mod 5 =0 then begin
    if DoReloadQueue then begin
      CacheQueue;
      if Assigned(OnQueueChange) then OnQueueChange();
    end; // if QueueHasChanged then begin

  end; // if MSCTimer.Tag mod 5 =0 then begin
end;

procedure TMServClient.EnqueueTopX(Count:Integer);
var
  ResI                                 : Integer;
  ResS                                 : String;
  s                                    : String;
  Fields                               : TStringArray;

  Idx, i                               : Integer;
  TracksToEnqueue                      : Array of record
    AlbumID, TrackID                     : Integer;
  end;
begin
  try
    CommandExec(stCmd, 'top '+IntToStr(Count), ResI, ResS);
    if ResI<>234 then
      raise Exception.Create('"top" failed: ['+IntToStr(ResI)+'] '+ResS);

    ExtractFirstLine(ResS);

    SetLength(TracksToEnqueue, 0);
    while ResS<>'' do begin
      s:=ExtractFirstLine(ResS);
      ExtractFieldsFromLine(s, Fields);
      if High(Fields)+1<7 then
        raise Exception.Create('"top" returned invalid number of fields!');

      Idx:=High(TracksToEnqueue)+1;
      SetLength(TracksToEnqueue, Idx+1);
      try
        TracksToEnqueue[Idx].AlbumID:=StrToInt(Fields[1]);
      except
        raise Exception.Create('"top" returned invalid data in field 1 ("AlbumID")!');
      end;

      try
        TracksToEnqueue[Idx].TrackID:=StrToInt(Fields[2]);
      except
        raise Exception.Create('"top" returned invalid data in field 2 ("TrackID")!');
      end;

    end; // while ResS<>'' do begin


    for i:=0 to High(TracksToEnqueue) do begin
      try
        Enqueue(TracksToEnqueue[i].AlbumID, TracksToEnqueue[i].TrackID);
      except
        // der Server liefert nicht existente Tracks zurück! daher Fehler ignorieren
      end;
    end;

  except
    on E:Exception do raise Exception.Create('TMServClient.EnqueueTopX: '+E.Message);
  end;
end;

procedure TMServClient.ChangePassword(const OldPW, NewPW:String);
var
  ResI                                 : Integer;
  ResS                                 : String;
begin
  try
    if (Pos(' ', NewPW)>0) or (Pos(#9, NewPW)>0) then
      raise Exception.Create('Invalid character in new password! Whitespaces are not allowed!');

    CommandExec(stCmd, 'password '+OldPW+' '+NewPW, ResI, ResS);
	 if ResI<>249 then
		raise Exception.Create('"password" failed: ['+IntToStr(ResI)+'] '+ResS);

    Self._User.Pwd:=NewPW;

  except
    on E:Exception do begin
      E.Message:='TMServClient.ChangePassword: '+E.Message;
      raise;
    end;
  end;
end;

function TMServClient.GetRandomSetting:Boolean;
var
  ResI   : Integer;
  ResS   : String;
begin
  try
	 Result:=False;
	 CommandExec(CmdSocket,'random',ResI,ResS);
	 if ResI=242 then begin
		Result:= (pos('on',ResS)>0);
	 end;
  except
	 on E:Exception do raise Exception.Create('TMServClient.Play: '+E.Message);
  end;
end;

procedure TMServClient.SetRandomSetting(RandomOn:Boolean);
var
  ResI   : Integer;
  ResS   : String;
begin
  try
	 if RandomOn then
		 CommandExec(CmdSocket,'random on',ResI,ResS)
	 else
		 CommandExec(CmdSocket,'random off',ResI,ResS);
	 // 267=changed, 410=unchanged
	 if (ResI<>267) and (ResI<>410) then
		raise Exception.Create('"random on/off" failed: ['+IntToStr(ResI)+'] '+ResS);
  except
	 on E:Exception do raise Exception.Create('TMServClient.Random: '+E.Message);
  end;
end;



procedure TMServClient.SetFilter(FilterOn:Boolean; FilterArg:String);
var
  ResI   : Integer;
  ResS   : String;
begin
  try
	 if not FilterOn then
		 CommandExec(CmdSocket,'filter off',ResI,ResS)
	 else
		 CommandExec(CmdSocket,'filter '+FilterArg,ResI,ResS);
	 // 269=changed,
	 if (ResI<>269) then
		raise Exception.Create('"filter on/off" '+filterArg+' failed: ['+IntToStr(ResI)+'] '+ResS);
  except
	 on E:Exception do raise Exception.Create('TMServClient.Filter: '+E.Message);
  end;
end;


//=================================================================================================
// UUDECODE
//=================================================================================================
procedure UUdecodeLine(UUin: string; var UUout: string);
const
  SP = #32;
  CR = #13;
  LF = #10;
  EOF= #27;
const
  B64: Array[0..63] of Char = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
type
  TTriplet = Array[0..2] of Byte;
  TKwartet = Array[0..3] of Byte;
var

	 i,j,k: Integer;
	 Ipos,E: Integer;
	 Kwartet: TKwartet;
	 Triplet: TTriplet;

	 procedure Kwartet2Triplet(Kwartet: TKwartet; var Triplet: TTriplet);
	 var
		i: Integer;
	 begin
		  for i:=0 to 3 do
		  begin
			 case Chr(Kwartet[i]) of
			  'A'..'Z': Kwartet[i] := 0 + Kwartet[i] - Ord('A') + Ord(SP);
			  'a'..'z': Kwartet[i] := 26+ Kwartet[i] - Ord('a') + Ord(SP);
			  '0'..'9': Kwartet[i] := 52+ Kwartet[i] - Ord('0') + Ord(SP);
					 '+': Kwartet[i] := 62+ Ord(SP);
					 '/': Kwartet[i] := 63+ Ord(SP)
			 end
		  end;
		Triplet[0] :=  ((Kwartet[0] - Ord(SP)) SHL 2) +
						  (((Kwartet[1] - Ord(SP)) AND $30) SHR 4);
		Triplet[1] := (((Kwartet[1] - Ord(SP)) AND $0F) SHL 4) +
						  (((Kwartet[2] - Ord(SP)) AND $3C) SHR 2);
		Triplet[2] := (((Kwartet[2] - Ord(SP)) AND $03) SHL 6) +
							((Kwartet[3] - Ord(SP)) AND $3F)
	 end {Kwartet2Triplet};

begin
	Ipos := 1;
	UUout:='';
	{ skip first character of each line (length)}
	Inc(Ipos);
	while Ipos<Length(UUin) do begin
		// hak een kwartetje af
		E := Ipos;
		j := 0;
		FillChar(Kwartet,SizeOf(TKwartet),#0);
		repeat
			Kwartet[j] := Ord(UUin[E]);
			Inc(j);
			Inc(E)
		until (j = SizeOf(TKwartet)) or (E>Length(UUin));
		Ipos:=Ipos+j; // verwijder kwartet
		// en bereken restultaat naar output
		Kwartet2Triplet(Kwartet,Triplet);
		for i:=0 to 2 do
			UUout:=UUout+Char(Triplet[i]);  
	end;
end;


end.
