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
CDDBServer(const BString & cddbServer)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
Query(uint32 cddbID,const scsi_toc_toc * toc,QueryResponseList & queryResponses)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
Read(const QueryResponseData & diskData,ReadResponseData & readResponse,bool verbose)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
Read(const BString & category,const BString & cddbID,const BString & artist,ReadResponseData & readResponse,bool verbose)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
_ParseAddress(const BString & cddbServer)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
_OpenConnection()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
_CloseConnection()352 CDDBServer::_CloseConnection()
353 {
354 if (!fConnected)
355 return;
356
357 fConnection.Close();
358 fConnected = false;
359 }
360
361
362 status_t
_SendCommand(const BString & command,BString & output)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*
_Track(ReadResponseData & response,uint32 track) const400 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