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