1/*
2 * Copyright 2008-2009, Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *              Bruno Albuquerque, bga@bug-br.org.br
7 */
8
9#include "cddb_server.h"
10
11#include <errno.h>
12#include <stdio.h>
13#include <stdlib.h>
14#include <unistd.h>
15
16
17static const char* kDefaultLocalHostName = "unknown";
18static const uint32 kDefaultPortNumber = 80;
19
20static const uint32 kFramesPerSecond = 75;
21static const uint32 kFramesPerMinute = kFramesPerSecond * 60;
22
23
24CDDBServer::CDDBServer(const BString& cddbServer):
25	fInitialized(false),
26	fConnected(false)
27{
28
29	// Set up local host name.
30	char localHostName[MAXHOSTNAMELEN + 1];
31	if (gethostname(localHostName,  MAXHOSTNAMELEN + 1) == 0) {
32		fLocalHostName = localHostName;
33	} else {
34		fLocalHostName = kDefaultLocalHostName;
35	}
36
37	// Set up local user name.
38	char* user = getenv("USER");
39	if (user == NULL) {
40		fLocalUserName = "unknown";
41	} else {
42		fLocalUserName = user;
43	}
44
45	// Set up server address;
46	if (_ParseAddress(cddbServer) == B_OK)
47		fInitialized = true;
48}
49
50
51status_t
52CDDBServer::Query(uint32 cddbId, const scsi_toc_toc* toc, BList* queryResponse)
53{
54	if (_OpenConnection() != B_OK)
55		return B_ERROR;
56
57	// Convert CDDB id to hexadecimal format.
58	char hexCddbId[9];
59	sprintf(hexCddbId, "%08" B_PRIx32 "", cddbId);
60
61	// Assemble the Query command.
62	int32 numTracks = toc->last_track + 1 - toc->first_track;
63
64	BString cddbCommand("cddb query ");
65	cddbCommand << hexCddbId << " " << numTracks << " ";
66
67	// Add track offsets in frames.
68	for (int32 i = 0; i < numTracks; ++i) {
69		const scsi_cd_msf& start = toc->tracks[i].start.time;
70
71		uint32 startFrameOffset = start.minute * kFramesPerMinute +
72			start.second * kFramesPerSecond + start.frame;
73
74		cddbCommand << startFrameOffset << " ";
75	}
76
77	// Add total disc time in seconds. Last track is lead-out.
78	const scsi_cd_msf& lastTrack = toc->tracks[numTracks].start.time;
79	uint32 totalTimeInSeconds = lastTrack.minute * 60 + lastTrack.second;
80	cddbCommand << totalTimeInSeconds;
81
82	BString output;
83	status_t result;
84	result = _SendCddbCommand(cddbCommand, &output);
85
86	if (result == B_OK) {
87		// Remove the header from the reply.
88		output.Remove(0, output.FindFirst("\r\n\r\n") + 4);
89
90		// Check status code.
91		BString statusCode;
92		output.MoveInto(statusCode, 0, 3);
93		if (statusCode == "210" || statusCode == "211") {
94			// TODO(bga): We can get around with returning the first result
95			// in case of multiple matches, but we most definitely needs a
96			// better handling of inexact matches.
97			if (statusCode == "211")
98				printf("Warning : Inexact match found.\n");
99
100			// Multiple results, remove the first line and parse the others.
101			output.Remove(0, output.FindFirst("\r\n") + 2);
102		} else if (statusCode == "200") {
103			// Remove the first char which is a left over space.
104			output.Remove(0, 1);
105		} else if (statusCode == "202") {
106			// No match found.
107			printf("Error : CDDB entry for id %s not found.\n", hexCddbId);
108
109			return B_ENTRY_NOT_FOUND;
110		} else {
111			// Something bad happened.
112			if (statusCode.Trim() != "") {
113				printf("Error : CDDB server status code is %s.\n",
114					statusCode.String());
115			} else {
116				printf("Error : Could not find any status code.\n");
117			}
118
119			return B_ERROR;
120		}
121
122		// Process all entries.
123		bool done = false;
124		while (!done) {
125			QueryResponseData* responseData = new QueryResponseData;
126
127			output.MoveInto(responseData->category, 0, output.FindFirst(" "));
128			output.Remove(0, 1);
129
130			output.MoveInto(responseData->cddbId, 0, output.FindFirst(" "));
131			output.Remove(0, 1);
132
133			output.MoveInto(responseData->artist, 0, output.FindFirst(" / "));
134			output.Remove(0, 3);
135
136			output.MoveInto(responseData->title, 0, output.FindFirst("\r\n"));
137			output.Remove(0, 2);
138
139			queryResponse->AddItem(responseData);
140
141			if (output == "" || output == ".\r\n") {
142				// All returned data was processed exit the loop.
143				done = true;
144			}
145		}
146	} else {
147		printf("Error sending CDDB command : \"%s\".\n", cddbCommand.String());
148	}
149
150	_CloseConnection();
151	return result;
152}
153
154
155status_t
156CDDBServer::Read(QueryResponseData* diskData, ReadResponseData* readResponse)
157{
158	if (_OpenConnection() != B_OK)
159		return B_ERROR;
160
161	// Assemble the Read command.
162	BString cddbCommand("cddb read ");
163	cddbCommand << diskData->category << " " << diskData->cddbId;
164
165	BString output;
166	status_t result;
167	result = _SendCddbCommand(cddbCommand, &output);
168
169	if (result == B_OK) {
170		// Remove the header from the reply.
171		output.Remove(0, output.FindFirst("\r\n\r\n") + 4);
172
173		// Check status code.
174		BString statusCode;
175		output.MoveInto(statusCode, 0, 3);
176		if (statusCode == "210") {
177			// Remove first line and parse the others.
178			output.Remove(0, output.FindFirst("\r\n") + 2);
179		} else {
180			// Something bad happened.
181			return B_ERROR;
182		}
183
184		// Process all entries.
185		bool done = false;
186		while (!done) {
187			if (output[0] == '#') {
188				// Comment. Remove it.
189				output.Remove(0, output.FindFirst("\r\n") + 2);
190				continue;
191			}
192
193			// Extract one line to reduce the scope of processing to it.
194			BString line;
195			output.MoveInto(line, 0, output.FindFirst("\r\n"));
196			output.Remove(0, 2);
197
198			// Obtain prefix.
199			BString prefix;
200			line.MoveInto(prefix, 0, line.FindFirst("="));
201			line.Remove(0, 1);
202
203			if (prefix == "DTITLE") {
204				// Disk title.
205				BString artist;
206				line.MoveInto(artist, 0, line.FindFirst(" / "));
207				line.Remove(0, 3);
208				readResponse->title = line;
209				readResponse->artist = artist;
210			} else if (prefix == "DYEAR") {
211				// Disk year.
212				char* firstInvalid;
213				errno = 0;
214				uint32 year = strtoul(line.String(), &firstInvalid, 10);
215				if ((errno == ERANGE &&
216					(year == (uint32)LONG_MAX || year == (uint32)LONG_MIN))
217					|| (errno != 0 && year == 0)) {
218					// Year out of range.
219					printf("Year out of range: %s\n", line.String());
220					return B_ERROR;
221				}
222
223				if (firstInvalid == line.String()) {
224					printf("Invalid year: %s\n", line.String());
225					return B_ERROR;
226				}
227
228				readResponse->year = year;
229			} else if (prefix == "DGENRE") {
230				// Disk genre.
231				readResponse->genre = line;
232			} else if (prefix.FindFirst("TTITLE") == 0) {
233				// Track title.
234				BString index;
235				prefix.MoveInto(index, 6, prefix.Length() - 6);
236
237				TrackData* trackData = new TrackData;
238
239				char* firstInvalid;
240				errno = 0;
241				uint32 track = strtoul(index.String(), &firstInvalid, 10);
242				if ((errno == ERANGE &&
243					(track == (uint32)LONG_MAX || track == (uint32)LONG_MIN))
244					|| (errno != 0 && track == 0)) {
245					// Track out of range.
246					printf("Track out of range: %s\n", index.String());
247					delete trackData;
248					return B_ERROR;
249				}
250
251				if (firstInvalid == index.String()) {
252					printf("Invalid track: %s\n", index.String());
253					delete trackData;
254					return B_ERROR;
255				}
256
257				trackData->trackNumber = track;
258
259				int32 pos = line.FindFirst(" / " );
260				if (pos != B_ERROR) {
261					// We have track specific artist information.
262					BString artist;
263					line.MoveInto(artist, 0, pos);
264					line.Remove(0, 3);
265					trackData->artist = artist;
266				} else {
267					trackData->artist = diskData->artist;
268				}
269
270				trackData->title = line;
271
272				(readResponse->tracks).AddItem(trackData);
273			}
274
275			if (output == "" || output == ".\r\n") {
276				// All returned data was processed exit the loop.
277				done = true;
278			}
279		}
280	} else {
281		printf("Error sending CDDB command : \"%s\".\n", cddbCommand.String());
282	}
283
284	_CloseConnection();
285	return B_OK;
286}
287
288
289status_t
290CDDBServer::_ParseAddress(const BString& cddbServer)
291{
292	// Set up server address.
293	int32 pos = cddbServer.FindFirst(":");
294	if (pos == B_ERROR) {
295		// It seems we do not have the address:port format. Use hostname as-is.
296		fCddbServerAddr.SetTo(cddbServer.String(), kDefaultPortNumber);
297		if (fCddbServerAddr.InitCheck() == B_OK)
298			return B_OK;
299	} else {
300		// Parse address:port format.
301		int32 port;
302		BString newCddbServer(cddbServer);
303		BString portString;
304		newCddbServer.MoveInto(portString, pos + 1,
305			newCddbServer.CountChars() - pos + 1);
306		if (portString.CountChars() > 0) {
307			char* firstInvalid;
308			errno = 0;
309			port = strtol(portString.String(), &firstInvalid, 10);
310			if ((errno == ERANGE && (port == LONG_MAX || port == LONG_MIN))
311				|| (errno != 0 && port == 0)) {
312				return B_ERROR;
313			}
314			if (firstInvalid == portString.String()) {
315				return B_ERROR;
316			}
317
318			newCddbServer.RemoveAll(":");
319			fCddbServerAddr.SetTo(newCddbServer.String(), port);
320			if (fCddbServerAddr.InitCheck() == B_OK)
321				return B_OK;
322		}
323	}
324
325	return B_ERROR;
326}
327
328
329status_t
330CDDBServer::_OpenConnection()
331{
332	if (!fInitialized)
333		return B_ERROR;
334
335	if (fConnected)
336		return B_OK;
337
338	if (fConnection.Connect(fCddbServerAddr) == B_OK) {
339		fConnected = true;
340		return B_OK;
341	}
342
343	return B_ERROR;
344}
345
346
347void
348CDDBServer::_CloseConnection()
349{
350	if (!fConnected)
351		return;
352
353	fConnection.Close();
354	fConnected = false;
355}
356
357
358status_t
359CDDBServer::_SendCddbCommand(const BString& command, BString* output)
360{
361	if (!fConnected)
362		return B_ERROR;
363
364	// Assemble full command string.
365	BString fullCommand;
366	fullCommand << command << "&hello=" << fLocalUserName << " " <<
367		fLocalHostName << " cddb_daemon 1.0&proto=6";
368
369	// Replace spaces by + signs.
370	fullCommand.ReplaceAll(" ", "+");
371
372	// And now add command header and footer.
373	fullCommand.Prepend("GET /~cddb/cddb.cgi?cmd=");
374	fullCommand << " HTTP 1.0\n\n";
375
376	int32 result = fConnection.Send((void*)fullCommand.String(),
377		fullCommand.Length());
378	if (result == fullCommand.Length()) {
379		BNetBuffer netBuffer;
380		while (fConnection.Receive(netBuffer, 1024) != 0) {
381			// Do nothing. Data is automatically appended to the NetBuffer.
382		}
383
384		// AppendString automatically adds the terminating \0.
385		netBuffer.AppendString("");
386
387		output->SetTo((char*)netBuffer.Data(), netBuffer.Size());
388		return B_OK;
389	}
390
391	return B_ERROR;
392}
393