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