///////////////////////////////////////////////////////////////////// // // Program.cs // // Developed by Broccoli Products Ltd // // Last Modified 6th May, 2010 // // using System; using System.Collections.Generic; using System.Text; using System.Threading; // System.Diagnostics - tracing output and assertion checks #if DEBUG using System.Diagnostics; #endif // #if DEBUG // System.IO - provides the FileStream object for reading and writing to disk using System.IO; // System.Net.Sockets - provides the Socket class transfer of data over a network using System.Net; using System.Net.Sockets; // System.Web - XML data containers and serialization using System.Xml; namespace BBCRadioDownloader { ///////////////////////////////////////////////////////////////// // declaration of Program class class Program { ///////////////////////////////////////////////////////////// // constants and enumerations // to access the BBC's online content, this application must pretend to be an iPhone. // An iPhone would have a specific browser (BrowserUserAgent) and hardware (PhoneUserAgent) signature const String BrowserUserAgent = "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us)AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7D11 Safari/528.16"; const String PhoneUserAgent = "Apple iPhone v1.1.4 CoreMedia v1.0.0.4A102"; // this is the bbc website address const String BBCHostName = "www.bbc.net.uk"; // comand line argument prefixes const String PIDPrefix = "-pid="; const String TragetFolderPrefix = "-targetfolder="; // instructions, and bits added onto the end of error messages const String PIDArgFormat = "Run application again with argument -PID=b00XXXX."; const String TargetFolderArgFormat = "To specify the target folder, run application again with the optional argument -targetfolder=\"XXXXXXX...\"."; // timeout in ms for socket waiting const Int32 TimeoutTicks = 12 * 1000; // download the file in chunks (BBC uses maximum chunk of <70Mb) // use smaller chunks in debug, to promote errors #if DEBUG const Int32 MaxChunkSize = 8 * 1024 * 1024; #else const Int32 MaxChunkSize = 32 * 1024 * 1024; #endif // #if DEBUG private enum eProgrammeType { None, Radio, // mp3 Television // mov } ////////////////////////////////////////////////////////////// // attributes // large buffer, used when reading data from sockets private static Int32 iBigBufferUsedLength = 0; private static byte[] bigBuffer = new byte[1024 * 1024]; ////////////////////////////////////////////////////////////// // main application function static void Main(string[] args) { // declaration of local variables bool bOk = true; // set to false when an error occurs // display message Console.WriteLine("BBCRadioDownloader started."); // A) read the application arguments for the PID and the target folder-path String strPID; // the BBC programme ID String strTargetFolderPath; // the folder where the downloaded file will be stored bOk = _ReadApplicationArguments(args, out strPID, out strTargetFolderPath); // B) get the epsiode ID for this programme ID String strEpisodeId = null; String strEpisodeTitle = null; String strEpisodeSummary = null; eProgrammeType ProgrammeType = eProgrammeType.None; if (bOk) bOk = _GetEpisodeDetails(strPID, out strEpisodeId, out strEpisodeTitle, out strEpisodeSummary, out ProgrammeType); // C) combine the episode title and target folder-path to make a unique filename for the saved file String strTargetFilepath = null; if (bOk) bOk = _MakeUniqueFilepath(strTargetFolderPath, strEpisodeTitle, ProgrammeType, out strTargetFilepath); // D) locate episode stream and get cookie String strCookie = null; String strStreamLocation = null; if (bOk) bOk = _GetEpisodeStreamLocationAndCookie(strPID, out strCookie, out strStreamLocation); // E) get the length of the stream Int32 iStreamLength = 0; if (bOk) bOk = _QueryStreamLength(strCookie, strStreamLocation, out iStreamLength); // F) download the stream to the target-file if (bOk) bOk = _DownloadStream(ProgrammeType, strCookie, strStreamLocation, strTargetFilepath, iStreamLength); // trace heading Console.WriteLine("\nBBCRadioDownloader finished."); Console.WriteLine("Click any key to close..."); while (!Console.KeyAvailable) { } } ////////////////////////////////////////////////////////////// // private static operations private static bool _ReadApplicationArguments(String[] args, out String strPID, out String strTargetFolderPath) { ///////////////////////////////////////////////////////////////////////// // scan the application command-line arguments for a programme ID (PID) // and a target folder-path. If no target folder-path is provided, assume "c:\\". // If no PID provide, ask user to enter one, otherwise cannot continue. // PID format expected as "-pid=b00XXXXXX". // Target folder-path expected as -targetdir=XXXX... // declare return variable bool bFtmp = true; // reset function parameters strPID = null; strTargetFolderPath = null; // display message Console.WriteLine("\n* * * Scanning Application Arguments * * *"); // loop through arguments for (Int32 i1 = 0; i1 < args.Length; i1++) { // if compare prefixes of the argument... if (args[i1].StartsWith(PIDPrefix, StringComparison.CurrentCultureIgnoreCase)) { // this is a PID strPID = args[i1].Substring(PIDPrefix.Length); } else if (args[i1].StartsWith(TragetFolderPrefix, StringComparison.CurrentCultureIgnoreCase)) { // this is a target folder-path strTargetFolderPath = args[i1].Substring(TragetFolderPrefix.Length); // chop off the quotes if ( (strTargetFolderPath.Length >= 2) && (strTargetFolderPath[0] == '"') && (strTargetFolderPath[strTargetFolderPath.Length - 1] == '"') ) strTargetFolderPath = strTargetFolderPath.Substring(1, strTargetFolderPath.Length - 2); } else { // message for user Console.WriteLine("Unknown command-line argument \"{0}\", ignored.", args[i1]); Console.WriteLine(PIDArgFormat); bFtmp = false; } } // if no PID was provided, get the user to enter one if (strPID == null) { // enter a PID Console.Write("Enter Programme ID (b00XXXXX): "); strPID = Console.ReadLine(); } // check the PID if ((strPID == null) || (strPID.Length < 8)) { // message for user and flag an error _WriteError("NO valid PID provided."); _WriteError(PIDArgFormat); bFtmp = false; } else { // check PID begins with "b00" if (!strPID.StartsWith("b00", StringComparison.CurrentCulture)) { // message for user and flag an error _WriteError("PID must start with \"b00\"."); _WriteError(PIDArgFormat); bFtmp = false; } else { // check that PID is lower-case alphanumeric for (Int32 i1 = 0; i1 < strPID.Length; i1++) { // if this char is not lower-case alphanumeric... if (!_IsValidBBCIdChar(strPID[i1])) { // message for user and flag an error _WriteError("PID must contain numbers (0-9) and lower-case letters (a-z)."); _WriteError(PIDArgFormat); bFtmp = false; // break out of loop break; } } } } // check the target folder-path if (strTargetFolderPath == null) { // use the default target folder-path strTargetFolderPath = "c:\\"; // message for user Console.WriteLine("No target folder provided. Assuming \"{0}\".", strTargetFolderPath); Console.WriteLine(TargetFolderArgFormat); } else { // check folder exists (do it in a try-catch for safety) try { // if folder does NOT exist... if (!Directory.Exists(strTargetFolderPath)) { // message for user and update flag an error _WriteError(String.Format("The target folder \"{0}\" cannot be found.", strTargetFolderPath)); _WriteError(TargetFolderArgFormat); bFtmp = false; } } catch (Exception x) { // message for user and update flag an error Console.WriteLine(x.Message); Console.WriteLine(TargetFolderArgFormat); bFtmp = false; } } // return variable return bFtmp; } private static bool _GetEpisodeDetails(String strPID, out String strEpisodeId, out String strEpisodeTitle, out String strEpisodeSummary, out eProgrammeType ProgrammeType) { ///////////////////////////////////////////////////////// // use a standard web-request-reponse to get the epsiode ID, // title and summary from the BBC's iPlayer website. The // epsiode data is requested using the PID. The returned data // is an XML file, which can be walked-through to find the episode details. // The data is found in the following paths: // playlist \ summary // playlist \ title // playlist \ item \ mediator \ identifier // constants const String UnexpectedErrorMsg = "Unxepcted error requested programme {0} details.\n{1}"; // declare return variable bool bFtmp = true; // reset function parameters strEpisodeId = null; strEpisodeTitle = null; strEpisodeSummary = null; ProgrammeType = eProgrammeType.None; // display message Console.WriteLine("\n* * * Requesting Episode Details * * *"); // format request url String strUrl = String.Format("http://www.bbc.co.uk/iplayer/playlist/{0}", strPID); Uri uri = new Uri(strUrl); // create request object (and set a timeout) HttpWebRequest myReq = (HttpWebRequest)WebRequest.Create(uri); myReq.Timeout = TimeoutTicks; // request in try-catch incase page not found HttpWebResponse response = null; try { // read request result response = myReq.GetResponse() as HttpWebResponse; } catch (WebException x) { // if this is a 404 (page not found)... if (x.Message.Contains("(404)")) { // message for user and update flag an error _WriteError(String.Format("Programme {0} details not found.", strPID)); _WriteError(String.Format("Check the PID {0} is a valid programme.", strPID)); bFtmp = false; } else { // message for user and update flag an error Console.WriteLine(UnexpectedErrorMsg, strPID, x.Message); bFtmp = false; } } catch (Exception x) { // message for user and update flag an error Console.WriteLine(UnexpectedErrorMsg, strPID, x.Message); bFtmp = false; } // if that worked... if (bFtmp) { #if DEBUG Trace.Assert(response != null); #endif // #if DEBUG // construct an xml reader object from the response using (XmlTextReader r = new XmlTextReader(response.GetResponseStream())) { // set whitespace handler r.WhitespaceHandling = WhitespaceHandling.None; // move to the "playlist" element if (r.ReadToDescendant("playlist")) { // move to first child-element if (r.Read()) { // loop through elements while (r.NodeType == XmlNodeType.Element) { // look for "title", "summary" and "item" elements if (String.Compare(r.Name, "summary", true) == 0) strEpisodeSummary = r.ReadElementContentAsString(); else if (String.Compare(r.Name, "title", true) == 0) strEpisodeTitle = r.ReadElementContentAsString(); else if (String.Compare(r.Name, "item", true) == 0) { // if the group matches... String strGroup = r.GetAttribute("group"); if (strGroup == strPID) { // get the kind of programme String strKind = r.GetAttribute("kind"); if (strKind == "radioProgramme") ProgrammeType = eProgrammeType.Radio; else if (strKind == "programme") ProgrammeType = eProgrammeType.Television; // if programme type known... if (ProgrammeType != eProgrammeType.None) { // find "mediator" element if (r.ReadToDescendant("mediator")) { // get episode identifier strEpisodeId = r.GetAttribute("identifier"); // break out of loop break; } } } // move on r.Skip(); } else { // move on to next sibling r.Skip(); } } // while-loop } // if } else { // message for user and update flag an error _WriteError(String.Format("Cannot find \"playlist\" for programme {0}.", strPID)); bFtmp = false; } } // if NO episode data found... if (strEpisodeId == null) { // message for user and update flag an error _WriteError(String.Format("No episode media information found for programme {0}.", strPID)); if (strEpisodeTitle != null) _WriteError("This programme may not be available for download."); bFtmp = false; } } // if that worked... if (bFtmp) { // display the results _WriteValue("Epsiode Id", strEpisodeId); _WriteValue("Type", (ProgrammeType == eProgrammeType.Radio) ? "RADIO PROGRAMME" : "TELEVISION PROGRAMME"); _WriteValue("Title", strEpisodeTitle); _WriteValue("Summary", strEpisodeSummary); } // return variable return bFtmp; } private static bool _GetEpisodeStreamLocationAndCookie(String strPID, out String strCookie, out String strStreamLocation) { ///////////////////////////////////////////////////////// // get the stream location and a cookie for our session. // Requests to the BBC website require a cookie. We // use a raw socket to make the HTTP request (instead of // the HttpWebRequst object) because we want control // over exactly what is contained in the request. // declare return variable bool bFtmp = true; // reset function parameters strCookie = null; strStreamLocation = null; // declaration of local variables Int32 iPosA, iPosB = 0; // display message Console.WriteLine("\n* * * Requesting Episode Stream From BBC * * *"); // compose HTTP request StringBuilder sb = new StringBuilder(); sb.AppendFormat("GET /mobile/iplayer/episode/{0} HTTP/1.1\r\n", strPID); sb.Append("Accept-Language: en\r\n"); sb.Append("Connection: keep-alive\r\n"); sb.Append("Accept: */*\r\n"); sb.AppendFormat("User-Agent: {0}\r\n", BrowserUserAgent); sb.Append("Host: www.bbc.co.uk\r\n"); sb.Append("Pragma: no-cache\r\n"); sb.Append("HeaderEnd: CRLF\r\n"); sb.Append("\r\n"); // send request and wait for returned data iBigBufferUsedLength = 0; bFtmp = _LowLevel_SendRawSocketRequest(BBCHostName, sb.ToString()); // if that worked... if (bFtmp) { #if DEBUG Trace.Assert(iBigBufferUsedLength > 0); #endif // #if DEBUG // convert the response into a readable string String strResponse = Encoding.ASCII.GetString(bigBuffer, 0, iBigBufferUsedLength); // read the cookie value strCookie = _LowLevel_GetCookieFromResponse(strResponse); if (strCookie == null) bFtmp = false; // if that worked... if (bFtmp) { // some programmes contain adult content and the user has to // confirm that they are over 16 to watch. This confirmation // is in the form on clicking a link on a form. We can simulate // this by posting the form content and retrieving the new cookie. // does this programme contain a guidance prompt iPosA = strResponse.IndexOf("id=\"guidanceprompt\""); if (iPosA != -1) { // display message for user Console.WriteLine("\nProgramme contains adult content.\nPosting confirmation of age."); // need to post confirmation to BBC that we are over the age of 16 sb = new StringBuilder(); sb.AppendFormat("POST /mobile/iplayer/episode/{0} HTTP/1.1\r\n", strPID); sb.Append("Accept-Language: en\r\n"); sb.Append("Connection: keep-alive\r\n"); sb.Append("Accept: */*\r\n"); sb.Append("Content-Type: application/x-www-form-urlencoded\r\n"); sb.AppendFormat("User-Agent: {0}\r\n", BrowserUserAgent); sb.AppendFormat("Cookie: {0}\r\n", strCookie); sb.Append("Content-Length: 30\r\n"); sb.Append("Host: www.bbc.co.uk\r\n"); sb.Append("Pragma: no-cache\r\n"); sb.Append("\r\n"); sb.Append("form=guidanceprompt&isOver16=1"); // post request and wait for returned data iBigBufferUsedLength = 0; bFtmp = _LowLevel_SendRawSocketRequest(BBCHostName, sb.ToString()); // if that worked... if (bFtmp) { // convert response to a string strResponse = Encoding.ASCII.GetString(bigBuffer, 0, iBigBufferUsedLength); // get the new cookie strCookie = _LowLevel_GetCookieFromResponse(strResponse); } } } // if that worked... if (bFtmp) { // pull the stream location of out the HTML response // look for a line beginning with "href="http://download.iplayer.bbc.co.uk//" iPosA = strResponse.IndexOf("href=\"http://download.iplayer.bbc.co.uk/"); if (iPosA == -1) { // display error, update error flag _WriteError("Cannot find stream-location in HTML response."); bFtmp = false; } else { // find end of response iPosB = strResponse.IndexOf("\"", iPosA + 64); if (iPosB == -1) { // display error, update error flag _WriteError("Cannot find end of stream-location in HTML response."); bFtmp = false; } else { // get the stream location strStreamLocation = strResponse.Substring(iPosA + 6, iPosB - iPosA - 6); } } } } // if that worked... if (bFtmp) { // display the results _WriteValue("Cookie", strCookie); _WriteValue("Stream Location", strStreamLocation); } // return variable return bFtmp; } private static bool _MakeUniqueFilepath(String strTargetFolderPath, String strEpisodeTitle, eProgrammeType ProgrammeType, out String strTargetFilepath) { ///////////////////////////////////////////////////////// // combine the target folder-path provided in the // application arguments with the epsiode title to make // a unique and valid file-path for the saved file. // declare return variable bool bFtmp = true; // reset function parameters strTargetFilepath = null; // display message Console.WriteLine("\n* * * Making Target Filepath * * *"); // convert the file-title into an episode title (don't use any characters // that would be invalid for a file-name) String strInvalidFileNameChars = new String(Path.GetInvalidFileNameChars()); StringBuilder sbFileTitle = new StringBuilder(); for (Int32 i1 = 0; i1 < strEpisodeTitle.Length; i1++) { // if this character is valid... if ( (strEpisodeTitle[i1] != '.') && (strInvalidFileNameChars.IndexOf(strEpisodeTitle[i1]) == -1) ) sbFileTitle.Append(strEpisodeTitle[i1]); } // get file extension String strFileExtension; if (ProgrammeType == eProgrammeType.Radio) strFileExtension = ".mp3"; else strFileExtension = ".mov"; // combine the file-title with with folder and extension. Use a counter to // make the file-title unique. Int32 iCounter = 0; String strFileName; while (true) { // compose file-path if (iCounter == 0) strFileName = String.Format("{0}{1}", sbFileTitle.ToString(), strFileExtension); else strFileName = String.Format("{0}, Copy {1}{2}", sbFileTitle.ToString(), iCounter, strFileExtension); strTargetFilepath = Path.Combine(strTargetFolderPath, strFileName); if (!File.Exists(strTargetFilepath)) break; // move on iCounter++; } // while-loop // if that worked... if (bFtmp) { // display the results _WriteValue("Target Filepath", strTargetFilepath); } // return variable return bFtmp; } private static bool _QueryStreamLength(String strCookie, String strStreamLocation, out Int32 iStreamLength) { ///////////////////////////////////////////////////////// // query the length of the stream by requesting bytes "0-1" // from the stream location. // declare return variable bool bFtmp = true; // reset function parameters iStreamLength = 0; // display message Console.WriteLine("\n* * * Querying Stream Length * * *"); // split location into location-host and location-path Uri uri = new Uri(strStreamLocation); // compose request StringBuilder sb = new StringBuilder(); sb.AppendFormat("GET {0} HTTP/1.1\r\n", uri.PathAndQuery); sb.Append("Accept-Language: en\r\n"); sb.Append("Connection: keep-alive\r\n"); sb.Append("Accept: */*\r\n"); sb.AppendFormat("User-Agent: {0}\r\n", PhoneUserAgent); sb.AppendFormat("Cookie: {0}\r\n", strCookie); sb.Append("Range: bytes=0-1\r\n"); sb.AppendFormat("Host: {0}\r\n", uri.Host); sb.Append("Pragma: no-cache\r\n"); sb.Append("\r\n"); // send request and wait for returned data iBigBufferUsedLength = 0; bFtmp = _LowLevel_SendRawSocketRequest(uri.Host, sb.ToString()); // if that worked... if (bFtmp) { #if DEBUG Trace.Assert(iBigBufferUsedLength > 0); #endif // #if DEBUG // pull the cookie out of the reponse String strResponse = Encoding.ASCII.GetString(bigBuffer, 0, iBigBufferUsedLength); // find length Int32 iPosA = strResponse.IndexOf("\r\nContent-Range: bytes 0-"); if (iPosA != -1) { // find end of length iPosA += 27; Int32 iPosB = strResponse.IndexOf("\r\n", iPosA + 1); if (iPosB != -1) { // get length String strLength = strResponse.Substring(iPosA, iPosB - iPosA); if (!Int32.TryParse(strLength, out iStreamLength)) { // display error and update return variable _WriteError(String.Format("Cannot translate stream length ({0}) into a valid number", strLength)); bFtmp = false; } } else { // display error and update return variable _WriteError("Cannot find end of content-range line."); bFtmp = false; } } else { // display error and update return variable _WriteError("Cannot find content-range line."); bFtmp = false; } } // if that worked... if (bFtmp) { // display the results _WriteValue( "Stream Length", String.Format("{0} bytes", iStreamLength.ToString("#,#")) ); } // return variable return bFtmp; } private static bool _DownloadStream(eProgrammeType ProgrammeType, String strCookie, String strStreamLocation, String strTargetFilepath, Int32 iStreamLength) { ///////////////////////////////////////////////////////// // Download the file. Open the target file, download // the file from the stream in chunks, up to MaxChunkSize // in size. // declare return variable bool bFtmp = true; // display message Console.WriteLine("\n* * * Downloading Stream Length * * *"); Console.WriteLine("(When download starts, press \"C\" to cancel.)"); // split location into location-host and location-path Uri uri = new Uri(strStreamLocation); // build a url, with a place-markers for the start and end of the download range StringBuilder sb = new StringBuilder(); sb.AppendFormat("GET {0} HTTP/1.1\r\n", uri.PathAndQuery); sb.Append("Accept-Language: en\r\n"); sb.Append("Connection: keep-alive\r\n"); sb.Append("Accept: */*\r\n"); sb.AppendFormat("User-Agent: {0}\r\n", PhoneUserAgent); sb.AppendFormat("Cookie: {0}\r\n", strCookie); sb.Append("Range: bytes=[RANGE_START]-[RANGE_END]\r\n"); sb.AppendFormat("Host: {0}\r\n", uri.Host); sb.Append("Pragma: no-cache\r\n"); sb.Append("\r\n"); String strRequestTemplate = sb.ToString(); // open the target file FileStream fsTarget = null; try { fsTarget = new FileStream(strTargetFilepath, FileMode.Create, FileAccess.Write); } catch (Exception x) { // display message and update return variable _WriteError(String.Format("Error opening target file \"{0}\".\n{1}", strTargetFilepath, x.Message)); bFtmp = false; } // if that worked... if (bFtmp) { // download the content in chunks Int32 iOffset = 0; while (iOffset < iStreamLength) { // download the next chunk bFtmp = _DownloadStream_GetChunk(fsTarget, ProgrammeType, uri.Host, strRequestTemplate, iOffset, iStreamLength); if (!bFtmp) break; // move on iOffset += MaxChunkSize; } // while-loop } // clean up if (fsTarget != null) { fsTarget.Flush(); fsTarget.Close(); fsTarget = null; } // return variable return bFtmp; } private static bool _DownloadStream_GetChunk(FileStream fsTarget, eProgrammeType ProgrammeType, String strHost, String strRequestTemplate, Int32 iOffset, Int32 iStreamLength) { ///////////////////////////////////////////////////////// // Download a chunk of the file from the BBC website. // Each chunk can be up to MaxChunkSize in length. After // requesting the chunk, the BBC server sends out the data // until the whole chunk has been received. Monitor the "C" // key for when the user wants to cancel. // declare return variable bool bFtmp = true; // calculate bytes to read Int32 iBytesExpected = Math.Min(MaxChunkSize, iStreamLength - iOffset); #if DEBUG Trace.TraceInformation("Chunk iOffset={0} iBytesExpected={1}", iOffset, iBytesExpected); #endif // #if DEBUG // create socket Socket socket = null; bFtmp = _LowLevel_OpenSocketConnection(strHost, out socket); // if that worked, send data to socket if (bFtmp) { String strRequest = strRequestTemplate.Replace("[RANGE_START]", iOffset.ToString()); Int32 iEndOfRange = iOffset + iBytesExpected - 1; strRequest = strRequest.Replace("[RANGE_END]", iEndOfRange.ToString()); bFtmp = _LowLevel_SendSocketData(socket, strRequest); } // if that worked... if (bFtmp) { // loop through response Int32 iDataBytesReceived = 0; while (true) { // read socket data iBigBufferUsedLength = 0; bFtmp = _LowLevel_ReadSocketData(socket); if (!bFtmp) break; if (iBigBufferUsedLength == 0) { // if nothing received for this chunk... if (iDataBytesReceived == 0) { // display error and update return variable _WriteError(String.Format("No data stream data returned at {0}.", iOffset)); bFtmp = false; } // break out of loop break; } // check for cancel if (_IsCancelled()) { // update return variable and break bFtmp = false; break; } // if this chunk begins with HTTP content, do not include it in the file Int32 iStartOfData = 0; if (iDataBytesReceived == 0) { // find end of http header iStartOfData = _FindEndOfHttpHeader(ProgrammeType, (iOffset == 0)); if (iStartOfData == -1) { // display error, update return variable and break _WriteError("Cannot find start of MP3/MOV data in stream data."); bFtmp = false; break; } } // if MP3 data found... if (iStartOfData < iBigBufferUsedLength) { // write data to file fsTarget.Write(bigBuffer, iStartOfData, iBigBufferUsedLength - iStartOfData); // calculate previous progress percent Int32 iLastPercentx100 = -1; if (fsTarget.Length == 0) iLastPercentx100 = _CalcProgress(iOffset + iDataBytesReceived, iStreamLength); // move on iDataBytesReceived += (iBigBufferUsedLength - iStartOfData); // calculate progress Int32 iNewPercentx100 = _CalcProgress(iOffset + iDataBytesReceived, iStreamLength); if (iLastPercentx100 != iNewPercentx100) Console.WriteLine("Downloading {0}%", iNewPercentx100 / 100.0); // check bytes received if (iDataBytesReceived > iBytesExpected) { // display error, update return variable and break _WriteError(String.Format("More data ({0} bytes) has been returned than was expected ({1} bytes).", iDataBytesReceived, iBytesExpected )); bFtmp = false; break; } else if (iDataBytesReceived == iBytesExpected) { #if DEBUG Trace.TraceInformation("End of exptected data"); #endif // #if DEBUG // break out of loop break; } } } // while-loop } // clean up if (socket != null) { socket.Shutdown(SocketShutdown.Both); socket.Close(); socket = null; } // return variable return bFtmp; } private static bool _LowLevel_SendRawSocketRequest(String strHost, String strRequest) { ///////////////////////////////////////////////////////// // send a socket request, and store the returned data // in the bigBuffer. Variable iBigBufferUsedLength will // store how much data was returned. // declare return variable bool bFtmp = true; // open socket connection Socket socket = null; bFtmp = _LowLevel_OpenSocketConnection(strHost, out socket); // if that worked, send socket data if (bFtmp) bFtmp = _LowLevel_SendSocketData(socket, strRequest); // if that worked, read socket data if (bFtmp) bFtmp = _LowLevel_ReadSocketData(socket); // destruct the socket... if (socket != null) { socket.Shutdown(SocketShutdown.Both); socket.Disconnect(false); socket.Close(); } // return variable return bFtmp; } private static bool _LowLevel_OpenSocketConnection(String strHost, out Socket socket) { ///////////////////////////////////////////////////////// // resolve address for host (strHost) and open a // socket connection. // declare return variable bool bFtmp = true; // reset fuction parameters socket = null; IPEndPoint ipe = null; try { // resolve address IPHostEntry hostEntry = Dns.GetHostEntry(strHost); IPAddress address = hostEntry.AddressList[0]; ipe = new IPEndPoint(address, 80); } catch (Exception x) { // display error and updae return variable _WriteError(String.Format("Cannot resolve network address \"{0}\".", strHost)); _WriteError(x.Message); bFtmp = false; } // if that worked... if (bFtmp) { try { // connect socket and set parameters socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); socket.Connect(ipe); socket.ReceiveTimeout = TimeoutTicks; } catch (Exception x) { // display error and updae return variable _WriteError(String.Format("Cannot open socket connection to \"{0}\".", strHost)); _WriteError(x.Message); bFtmp = false; } } // return variable return bFtmp; } private static bool _LowLevel_ReadSocketData(Socket socket) { ///////////////////////////////////////////////////////// // read data waiting on the socket into the bigBuffer. // Called in a loop in case not all socket data arrives // at once. Wrapped in try-catch to trap reading exceptions. // Monitor the C key in case user cancels operation. // declare return variable bool bFtmp = true; // resert the big-buffer used length iBigBufferUsedLength = 0; // make sure the socket timeout is set socket.ReceiveTimeout = TimeoutTicks; // loop through data while (true) { // use try-catch to trap timeout Int32 iBytesRead = 0; try { // read data and add to buffer iBytesRead = socket.Receive(bigBuffer, iBigBufferUsedLength, bigBuffer.Length - iBigBufferUsedLength, SocketFlags.None); // update used length of buffer iBigBufferUsedLength += iBytesRead; // debug trace #if DEBUG if (iBytesRead > 0) Trace.TraceInformation("Received {0} bytes on socket.", iBytesRead.ToString("#,#")); #endif // #if DEBUG } catch (SocketException x) { // if timeout... if (x.ErrorCode == 10060) { // timeout - that's ok } else { // display error and update return variable _WriteError("Unexpected socket error waiting for data."); _WriteError(x.Message); bFtmp = false; } } catch (Exception x) { // display error and update return variable _WriteError("Unexpected error waiting for data."); _WriteError(x.Message); bFtmp = false; } // break when no data loaded if (iBytesRead == 0) break; // break if buffer is full... if (iBigBufferUsedLength == bigBuffer.Length) break; // check for cancel if (_IsCancelled()) { // update return variable and break bFtmp = false; break; } } // while-loop // return variable return bFtmp; } private static bool _LowLevel_SendSocketData(Socket socket, String strRequest) { ///////////////////////////////////////////////////////// // send a request by converting to a byte array and calling // a socket function. Wrapped in try-catch to identify // sending exceptions. // declare return variable bool bFtmp = true; try { // send query Byte[] sendBuffer = Encoding.ASCII.GetBytes(strRequest); socket.Send(sendBuffer, sendBuffer.Length, 0); } catch (Exception x) { // display error and update return variable _WriteError("Unexpected error sending data."); _WriteError(x.Message); bFtmp = false; } // return variable return bFtmp; } private static Int32 _FindEndOfHttpHeader(eProgrammeType ProgrammeType, bool bStartOfFile) { ///////////////////////////////////////////////////////// // HTTP content is not part of the MP3 or MOV file. If a chunk // starts with HTTP content, skip to the end of the content. // shortcuts if ( (bigBuffer[0] != 'H') || (bigBuffer[1] != 'T') || (bigBuffer[2] != 'T') || (bigBuffer[3] != 'P') ) return 0; // shortcuts if (iBigBufferUsedLength < 8) return -1; // declare return variable Int32 iFtmp = -1; // find 0D 0A 0D 0A marker, followed by data Int32 iPos = 0; while ((iPos + 8) < iBigBufferUsedLength) { // if this is the end of the HTTP, and the start of the data... if ( (bigBuffer[iPos] == 0x0D) && (bigBuffer[iPos + 1] == 0x0A) && (bigBuffer[iPos + 2] == 0x0D) && (bigBuffer[iPos + 3] == 0x0A) ) { // if the start of the file, look at the next data that follows... if ( (!bStartOfFile) || ( (ProgrammeType == eProgrammeType.Radio) && (iBigBufferUsedLength > (iPos + 7)) && (bigBuffer[iPos + 4] == 'I') && (bigBuffer[iPos + 5] == 'D') && (bigBuffer[iPos + 6] == '3') && (bigBuffer[iPos + 7] == 0x03) ) || ( (ProgrammeType == eProgrammeType.Television) && (iBigBufferUsedLength > (iPos + 8)) && (bigBuffer[iPos + 4] == 0x00) && (bigBuffer[iPos + 5] == 0x00) && (bigBuffer[iPos + 6] == 0x00) && (bigBuffer[iPos + 7] == 0x14) ) ) { #if DEBUG Trace.TraceInformation("End of HTTP Found"); #endif // #if DEBUG // update return variable and break out of loop iFtmp = iPos + 4; break; } } // move on iPos++; } // while-loop // return variable return iFtmp; } private static Int32 _CalcProgress(Int32 iTotalMP3BytesReceived, Int32 iStreamLength) { // calulate the percentage progress of download progress and multiply by 100 // return calculated percentage return Convert.ToInt32(Math.Round((10000.0 * iTotalMP3BytesReceived) / iStreamLength)); } private static void _WriteError(String strMsg) { ///////////////////////////////////////////////////////// // write error messages in red // set text to red Console.ForegroundColor = ConsoleColor.Red; { // write text Console.WriteLine("!!! {0}", strMsg); } Console.ForegroundColor = ConsoleColor.Gray; } private static void _WriteValue(String strLabel, String strValue) { ///////////////////////////////////////////////////////// // write values with label in grey and value in bright-white // write label Console.Write(strLabel); Console.Write(": "); // write value Console.ForegroundColor = ConsoleColor.White; Console.Write(strValue); Console.ForegroundColor = ConsoleColor.Gray; // newline Console.WriteLine(); } private static bool _IsCancelled() { ///////////////////////////////////////////////////////// // check the C button for cancellation // declare return variable bool bFtmp = false; // check for key press if (Console.KeyAvailable) { // get key details ConsoleKeyInfo key = Console.ReadKey(true); if (key.Key == ConsoleKey.C) { // display error and update return variable _WriteError("Operation cancelled by user."); bFtmp = true; } } // return variable return bFtmp; } private static bool _IsValidBBCIdChar(char c1) { ///////////////////////////////////////////////////////// // return true if character is valid for a BBC id // char must be lower-case alphanumeric return ( ((c1 >= 'a') && (c1 <= 'z')) || ((c1 >= '0') && (c1 <= '9')) ); } private static String _LowLevel_GetCookieFromResponse(String strResponse) { ///////////////////////////////////////////////////////// // pull cokie data out of an HTML response string // declare return variable String strFtmp = null; // find cookie Int32 iPosA = strResponse.IndexOf("Set-Cookie: "); if (iPosA != -1) { // find end of cookie iPosA += 12; Int32 iPosB = strResponse.IndexOf("\r\n", iPosA + 1); if (iPosB != -1) { // get cookie strFtmp = strResponse.Substring(iPosA, iPosB - iPosA); #if DEBUG Trace.TraceInformation("Cookie={0}", strFtmp); #endif // #if DEBUG } else { // display error _WriteError("Cannot find end of cookie line in HTML response."); } } else { // display error _WriteError("Cannot locate cookie line in HTML response."); } // return variable return strFtmp; } } }