xref: /haiku/src/bin/cddb_lookup/cddb_server.cpp (revision 02354704729d38c3b078c696adc1bbbd33cbcf72)
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 
18 static const char* kDefaultLocalHostName = "unknown";
19 static const uint32 kDefaultPortNumber = 80;
20 
21 static const uint32 kFramesPerSecond = 75;
22 static const uint32 kFramesPerMinute = kFramesPerSecond * 60;
23 
24 
25 CDDBServer::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 
51 status_t
52 CDDBServer::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 
154 status_t
155 CDDBServer::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 
163 status_t
164 CDDBServer::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 
293 status_t
294 CDDBServer::_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 
333 status_t
334 CDDBServer::_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 
351 void
352 CDDBServer::_CloseConnection()
353 {
354 	if (!fConnected)
355 		return;
356 
357 	fConnection.Close();
358 	fConnected = false;
359 }
360 
361 
362 status_t
363 CDDBServer::_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 
399 TrackData*
400 CDDBServer::_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