⛏️ index : haiku.git

/*
 * Copyright 2008-2016, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Bruno Albuquerque, bga@bug-br.org.br
 */


#include "cddb_server.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


static const char* kDefaultLocalHostName = "unknown";
static const uint32 kDefaultPortNumber = 80;

static const uint32 kFramesPerSecond = 75;
static const uint32 kFramesPerMinute = kFramesPerSecond * 60;


CDDBServer::CDDBServer(const BString& cddbServer)
	:
	fInitialized(false),
	fConnected(false)
{
	// Set up local host name.
	char localHostName[MAXHOSTNAMELEN + 1];
	if (gethostname(localHostName,  MAXHOSTNAMELEN + 1) == 0) {
		fLocalHostName = localHostName;
	} else {
		fLocalHostName = kDefaultLocalHostName;
	}

	// Set up local user name.
	char* user = getenv("USER");
	if (user == NULL)
		fLocalUserName = "unknown";
	else
		fLocalUserName = user;

	// Set up server address;
	if (_ParseAddress(cddbServer) == B_OK)
		fInitialized = true;
}


status_t
CDDBServer::Query(uint32 cddbID, const scsi_toc_toc* toc,
	QueryResponseList& queryResponses)
{
	if (_OpenConnection() != B_OK)
		return B_ERROR;

	// Convert CDDB id to hexadecimal format.
	char hexCddbId[9];
	sprintf(hexCddbId, "%08" B_PRIx32, cddbID);

	// Assemble the Query command.
	int32 numTracks = toc->last_track + 1 - toc->first_track;

	BString cddbCommand("cddb query ");
	cddbCommand << hexCddbId << " " << numTracks << " ";

	// Add track offsets in frames.
	for (int32 i = 0; i < numTracks; ++i) {
		const scsi_cd_msf& start = toc->tracks[i].start.time;

		uint32 startFrameOffset = start.minute * kFramesPerMinute +
			start.second * kFramesPerSecond + start.frame;

		cddbCommand << startFrameOffset << " ";
	}

	// Add total disc time in seconds. Last track is lead-out.
	const scsi_cd_msf& lastTrack = toc->tracks[numTracks].start.time;
	uint32 totalTimeInSeconds = lastTrack.minute * 60 + lastTrack.second;
	cddbCommand << totalTimeInSeconds;

	BString output;
	status_t result = _SendCommand(cddbCommand, output);
	if (result == B_OK) {
		// Remove the header from the reply.
		output.Remove(0, output.FindFirst("\r\n\r\n") + 4);

		// Check status code.
		BString statusCode;
		output.MoveInto(statusCode, 0, 3);
		if (statusCode == "210" || statusCode == "211") {
			// TODO(bga): We can get around with returning the first result
			// in case of multiple matches, but we most definitely need a
			// better handling of inexact matches.
			if (statusCode == "211")
				printf("Warning : Inexact match found.\n");

			// Multiple results, remove the first line and parse the others.
			output.Remove(0, output.FindFirst("\r\n") + 2);
		} else if (statusCode == "200") {
			// Remove the first char which is a left over space.
			output.Remove(0, 1);
		} else if (statusCode == "202") {
			// No match found.
			printf("Error : CDDB entry for id %s not found.\n", hexCddbId);

			return B_ENTRY_NOT_FOUND;
		} else {
			// Something bad happened.
			if (statusCode.Trim() != "") {
				printf("Error : CDDB server status code is %s.\n",
					statusCode.String());
			} else {
				printf("Error : Could not find any status code.\n");
			}

			return B_ERROR;
		}

		// Process all entries.
		bool done = false;
		while (!done) {
			QueryResponseData* responseData = new QueryResponseData;

			output.MoveInto(responseData->category, 0, output.FindFirst(" "));
			output.Remove(0, 1);

			output.MoveInto(responseData->cddbID, 0, output.FindFirst(" "));
			output.Remove(0, 1);

			output.MoveInto(responseData->artist, 0, output.FindFirst(" / "));
			output.Remove(0, 3);

			output.MoveInto(responseData->title, 0, output.FindFirst("\r\n"));
			output.Remove(0, 2);

			queryResponses.AddItem(responseData);

			if (output == "" || output == ".\r\n") {
				// All returned data was processed exit the loop.
				done = true;
			}
		}
	} else {
		printf("Error sending CDDB command : \"%s\".\n", cddbCommand.String());
	}

	_CloseConnection();
	return result;
}


status_t
CDDBServer::Read(const QueryResponseData& diskData,
	ReadResponseData& readResponse, bool verbose)
{
	return Read(diskData.category, diskData.cddbID, diskData.artist,
		readResponse, verbose);
}


status_t
CDDBServer::Read(const BString& category, const BString& cddbID,
	const BString& artist, ReadResponseData& readResponse, bool verbose)
{
	if (_OpenConnection() != B_OK)
		return B_ERROR;

	// Assemble the Read command.
	BString cddbCommand("cddb read ");
	cddbCommand << category << " " << cddbID;

	BString output;
	status_t result = _SendCommand(cddbCommand, output);
	if (result == B_OK) {
		if (verbose)
			puts(output);

		// Remove the header from the reply.
		output.Remove(0, output.FindFirst("\r\n\r\n") + 4);

		// Check status code.
		BString statusCode;
		output.MoveInto(statusCode, 0, 3);
		if (statusCode == "210") {
			// Remove first line and parse the others.
			output.Remove(0, output.FindFirst("\r\n") + 2);
		} else {
			// Something bad happened.
			return B_ERROR;
		}

		// Process all entries.
		bool done = false;
		while (!done) {
			if (output[0] == '#') {
				// Comment. Remove it.
				output.Remove(0, output.FindFirst("\r\n") + 2);
				continue;
			}

			// Extract one line to reduce the scope of processing to it.
			BString line;
			output.MoveInto(line, 0, output.FindFirst("\r\n"));
			output.Remove(0, 2);

			// Obtain prefix.
			BString prefix;
			line.MoveInto(prefix, 0, line.FindFirst("="));
			line.Remove(0, 1);

			if (prefix == "DTITLE") {
				// Disk title.
				BString artist;
				line.MoveInto(artist, 0, line.FindFirst(" / "));
				line.Remove(0, 3);
				readResponse.title = line;
				readResponse.artist = artist;
			} else if (prefix == "DYEAR") {
				// Disk year.
				char* firstInvalid;
				errno = 0;
				uint32 year = strtoul(line.String(), &firstInvalid, 10);
				if ((errno == ERANGE &&
					(year == (uint32)LONG_MAX || year == (uint32)LONG_MIN))
					|| (errno != 0 && year == 0)) {
					// Year out of range.
					printf("Year out of range: %s\n", line.String());
					year = 0;
				}

				if (firstInvalid == line.String()) {
					printf("Invalid year: %s\n", line.String());
					year = 0;
				}

				readResponse.year = year;
			} else if (prefix == "DGENRE") {
				// Disk genre.
				readResponse.genre = line;
			} else if (prefix.FindFirst("TTITLE") == 0) {
				// Track title.
				BString index;
				prefix.MoveInto(index, 6, prefix.Length() - 6);

				char* firstInvalid;
				errno = 0;
				uint32 track = strtoul(index.String(), &firstInvalid, 10);
				if (errno != 0 || track > 99) {
					// Track out of range.
					printf("Track out of range: %s\n", index.String());
					return B_ERROR;
				}

				if (firstInvalid == index.String()) {
					printf("Invalid track: %s\n", index.String());
					return B_ERROR;
				}

				BString trackArtist;
				int32 pos = line.FindFirst(" / ");
				if (pos >= 0 && artist.ICompare("Various") == 0) {
					// Disk is set to have a compilation artist and
					// we have track specific artist information.
					line.MoveInto(trackArtist, 0, pos);
						// Move artist information from line to artist.
					line.Remove(0, 3);
						// Remove " / " from line.
				} else {
					trackArtist = artist;
				}

				TrackData* trackData = _Track(readResponse, track);
				trackData->artist += trackArtist;
				trackData->title += line;
			}

			if (output == "" || output == ".\r\n") {
				// All returned data was processed exit the loop.
				done = true;
			}
		}
	} else {
		printf("Error sending CDDB command : \"%s\".\n", cddbCommand.String());
	}

	_CloseConnection();
	return B_OK;
}


status_t
CDDBServer::_ParseAddress(const BString& cddbServer)
{
	// Set up server address.
	int32 pos = cddbServer.FindFirst(":");
	if (pos == B_ERROR) {
		// It seems we do not have the address:port format. Use hostname as-is.
		fServerHostname.SetTo(cddbServer);
		fServerPort = kDefaultPortNumber;
		return B_OK;
	} else {
		// Parse address:port format.
		int32 port;
		BString newCddbServer(cddbServer);
		BString portString;
		newCddbServer.MoveInto(portString, pos + 1,
			newCddbServer.CountChars() - pos + 1);
		if (portString.CountChars() > 0) {
			char* firstInvalid;
			errno = 0;
			port = strtol(portString.String(), &firstInvalid, 10);
			if ((errno == ERANGE && (port == INT32_MAX || port == INT32_MIN))
				|| (errno != 0 && port == 0)) {
				return B_ERROR;
			}
			if (firstInvalid == portString.String()) {
				return B_ERROR;
			}

			newCddbServer.RemoveAll(":");
			fServerHostname.SetTo(newCddbServer);
			fServerPort = port;
			return B_OK;
		}
	}

	return B_ERROR;
}


status_t
CDDBServer::_OpenConnection()
{
	if (!fInitialized)
		return B_ERROR;

	if (fConnected)
		return B_OK;

	if (fServerAddress.InitCheck() != B_OK) {
		// This performs a DNS lookup, hence why we don't do it earlier.
		if (fServerAddress.SetTo(fServerHostname, fServerPort) != B_OK)
			return B_ERROR;
	}

	if (fConnection.Connect(fServerAddress) == B_OK) {
		fConnected = true;
		return B_OK;
	}

	return B_ERROR;
}


void
CDDBServer::_CloseConnection()
{
	if (!fConnected)
		return;

	fConnection.Close();
	fConnected = false;
}


status_t
CDDBServer::_SendCommand(const BString& command, BString& output)
{
	if (!fConnected)
		return B_ERROR;

	// Assemble full command string.
	BString fullCommand;
	fullCommand << command << "&hello=" << fLocalUserName << " " <<
		fLocalHostName << " cddb_lookup 1.0&proto=6";

	// Replace spaces by + signs.
	fullCommand.ReplaceAll(" ", "+");

	// And now add command header and footer.
	fullCommand.Prepend("GET /~cddb/cddb.cgi?cmd=");
	fullCommand << " HTTP/1.0\n\n";

	int32 result = fConnection.Send((void*)fullCommand.String(),
		fullCommand.Length());
	if (result == fullCommand.Length()) {
		BNetBuffer netBuffer;
		while (fConnection.Receive(netBuffer, 1024) != 0) {
			// Do nothing. Data is automatically appended to the NetBuffer.
		}

		// AppendString automatically adds the terminating \0.
		netBuffer.AppendString("");

		output.SetTo((char*)netBuffer.Data(), netBuffer.Size());
		return B_OK;
	}

	return B_ERROR;
}


TrackData*
CDDBServer::_Track(ReadResponseData& response, uint32 track) const
{
	for (int32 i = 0; i < response.tracks.CountItems(); i++) {
		TrackData* trackData = response.tracks.ItemAt(i);
		if (trackData->trackNumber == track)
			return trackData;
	}

	TrackData* trackData = new TrackData();
	trackData->trackNumber = track;
	response.tracks.AddItem(trackData);

	return trackData;
}