#region CPL License /* Nuclex Framework Copyright (C) 2002-2009 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using System.Text; using Nuclex.Networking.Exceptions; using Nuclex.Support; using Nuclex.Support.Tracking; using Nuclex.Support.Scheduling; namespace Nuclex.Audio.Metadata.Requests { /// /// Queries the CDDB server for entries matching the provided informations /// internal class CddbQueryRequest : Request, IAbortable, IProgressReporter { /// ASCII code for the space character private const char SP = ' '; /// /// Delegate used to notify the receiver of the active protocol level /// /// Active protocol level on the server public delegate void ProtocolLevelNotificationDelegate(int activeLevel); /// Triggered when the status of the process changes public event EventHandler AsyncProgressChanged; /// Initializes a new CDDB disc query request /// /// Protocol used to communication with the CDDB server /// /// /// Total length of the CD (from the beginning of the first track to the end of the /// last track) in seconds /// /// /// Offsets of the individual tracks on the CD in seconds /// public CddbQueryRequest( CddbProtocol protocol, int discLengthSeconds, int[] trackOffsetsSeconds ) { this.protocol = protocol; this.discLengthSeconds = discLengthSeconds; this.trackOffsetsSeconds = trackOffsetsSeconds; } /// Aborts the running process. Can be called from any thread. /// /// The receiver should honor the abort request and stop whatever it is /// doing as soon as possible. The method does not impose any requirement /// on the timeliness of the reaction of the running process, but implementers /// are advised to not ignore the abort request and urged to try and design /// their code in such a way that it can be stopped in a reasonable time /// (eg. within 1 second of the abort request). /// public void AsyncAbort() { if(this.protocol != null) { CddbProtocol theProtocol = this.protocol; this.protocol = null; this.exception = new AbortedException("Aborted on user request"); theProtocol.Dispose(); } } /// Starts the asynchronous execution of the request public void Start() { ThreadPool.QueueUserWorkItem(new WaitCallback(execute)); } /// /// Allows the specific request implementation to re-throw an exception if /// the background process finished unsuccessfully /// protected override void ReraiseExceptions() { if(this.exception != null) { throw this.exception; } } /// /// Allows the specific request implementation to re-throw an exception if /// the background process finished unsuccessfully /// protected override Cddb.Disc[] GatherResults() { return this.results; } /// Triggers the AsyncProgressChanged event /// New progress to report to the event subscribers protected virtual void OnProgressAchieved(float progress) { EventHandler copy = AsyncProgressChanged; if(copy != null) { copy(this, new ProgressReportEventArgs(progress)); } } /// Called asynchronously to execute the request /// Not used private void execute(object state) { lock(this.protocol.SyncRoot) { try { sendQuery(); // The first reply will indicate the status of the request. string statusLine = this.protocol.ReceiveLine(5000); int statusCode = CddbProtocol.GetStatusCode(statusLine); switch(statusCode) { case 200: { // Exact match found this.results = new Cddb.Disc[1] { decodeDiscFromStatusLine(statusLine) }; break; } case 202: { // No matches found this.results = new Cddb.Disc[0]; break; } case 211: { // Inexact matches found this.results = receiveDiscs(); break; } default: { this.exception = exceptionFromQueryStatus( statusCode, statusLine.Substring((statusLine.Length >= 4) ? 4 : 3) ); break; } } } catch(Exception exception) { if(!(this.exception is AbortedException)) { this.exception = exception; } } } OnAsyncEnded(); } /// Sends the query to the CDDB server private void sendQuery() { StringBuilder builder = new StringBuilder(192); // Build the initial query string consisting of the command, CDDB disc id // and the number of tracks on the CD builder.AppendFormat( "cddb query {0:x8} {1} ", Cddb.CalculateDiscId(this.discLengthSeconds, this.trackOffsetsSeconds), this.trackOffsetsSeconds.Length ); // Append the start offsets for(int index = 0; index < this.trackOffsetsSeconds.Length; ++index) { builder.AppendFormat("{0} ", this.trackOffsetsSeconds[index] * 75); } builder.Append(this.discLengthSeconds); this.protocol.SendLine(builder.ToString(), 5000); } /// Decodes disc informations directly from the status line /// Status line containing disc informations /// The decoded disc informations private Cddb.Disc decodeDiscFromStatusLine(string statusLine) { return decodeDisc(statusLine, 4); } /// Receives the disc informations being sent by the server /// All disc information records the server has transmitted private Cddb.Disc[] receiveDiscs() { List discList = new List(); for(; ; ) { string line = this.protocol.ReceiveLine(5000); if(line == ".") { break; } discList.Add(decodeDisc(line, 0)); } // All genres received, convert the list into an array that can be returned // to the owner of the request. return discList.ToArray(); } /// Decodes the disc informations sent by the CDDB server /// Line containing the disc informations /// /// Characer Index at which the disc informations begin /// /// The decoded CDDB disc informations private Cddb.Disc decodeDisc(string line, int startIndex) { // Locate where in the string the current protocol level starts int categoryEndIndex = -1; bool hasCategory = (line.Length >= startIndex) && ((categoryEndIndex = line.IndexOf(SP, startIndex)) != -1); if(!hasCategory) { throw makeBadResponseException("missing category name in query result"); } // Locate where in the string the current protocol level starts int discIdEndIndex = -1; bool hasDiscId = (line.Length >= categoryEndIndex + 1) && ((discIdEndIndex = line.IndexOf(SP, categoryEndIndex + 1)) != -1); if(!hasDiscId) { throw makeBadResponseException("missing disc id in query result"); } bool hasDiscTitle = (line.Length >= discIdEndIndex + 1) && (line[discIdEndIndex] == SP); if(!hasDiscTitle) { throw makeBadResponseException("missing disc title in query result"); } string discId = line.Substring( categoryEndIndex + 1, discIdEndIndex - categoryEndIndex - 1 ); Cddb.DTitle artistAndAlbum = Cddb.SplitDiscTitle( line.Substring(discIdEndIndex + 1) ); return new Cddb.Disc( line.Substring(startIndex, categoryEndIndex - startIndex), Convert.ToInt32(discId, 16), artistAndAlbum.Artist, artistAndAlbum.Title ); } /// Constructs a new BadResponseException /// /// Detailed reason to show in the exception message in addition to the standard /// bad response message /// /// The newly constructed exception private static BadResponseException makeBadResponseException(string detailedReason) { return new BadResponseException( string.Format("Malformed response from CDDB server ({0})", detailedReason) ); } /// /// Generates an exception from the status code in the reply to a disc query /// /// Status code provided by the server /// Response returned by the server /// /// The exception for the server status code or null if the status code /// indicates success /// private static Exception exceptionFromQueryStatus(int statusCode, string message) { switch(statusCode) { case 403: { return new Exceptions.DatabaseEntryCorruptException(message); } case 409: { return new Exceptions.NoHandshakeException(message); } default: { return new BadResponseException( string.Format( "Bad response from CDDB server: invalid status code '{0}', " + "server message is '{1}'", statusCode, message ) ); } } } /// Exception that has occured during asynchronous processing private volatile Exception exception; /// Protocol used to communicate with the CDDB server private volatile CddbProtocol protocol; /// Total length of the CD in seconds private int discLengthSeconds; /// Offsets of the tracks on the CD in seconds private int[] trackOffsetsSeconds; /// Results returned by the CDDB server private volatile Cddb.Disc[] results; } } // namespace Nuclex.Audio.Metadata.Requests