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