1 /* 2 * Copyright 2008-2016, Haiku, Inc. All Rights Reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Axel Dörfler, axeld@pinc-software.de 7 * Bruno Albuquerque, bga@bug-br.org.br 8 */ 9 10 11 #include <getopt.h> 12 #include <stdio.h> 13 #include <stdlib.h> 14 #include <string.h> 15 16 #include <Application.h> 17 #include <Directory.h> 18 #include <Entry.h> 19 #include <fs_info.h> 20 #include <Message.h> 21 #include <Volume.h> 22 #include <VolumeRoster.h> 23 24 #include <scsi_cmds.h> 25 26 #include "cddb_server.h" 27 28 29 class CDDBLookup : public BApplication { 30 public: 31 CDDBLookup(); 32 virtual ~CDDBLookup(); 33 34 void LookupAll(CDDBServer& server, bool dumpOnly, 35 bool verbose); 36 status_t Lookup(CDDBServer& server, const char* path, 37 bool dumpOnly, bool verbose); 38 status_t Lookup(CDDBServer& server, const dev_t device, 39 bool dumpOnly, bool verbose); 40 status_t Dump(CDDBServer& server, const char* category, 41 const char* cddbID, bool verbose); 42 43 private: 44 bool _ReadTOC(const dev_t device, uint32* cddbID, 45 scsi_toc_toc* toc) const; 46 const QueryResponseData* 47 _SelectResult( 48 const QueryResponseList& responses) const; 49 status_t _WriteCDData(dev_t device, 50 const QueryResponseData& diskData, 51 const ReadResponseData& readResponse); 52 void _Dump(const ReadResponseData& readResponse) 53 const; 54 }; 55 56 57 static struct option const kLongOptions[] = { 58 {"info", required_argument, 0, 'i'}, 59 {"dump", no_argument, 0, 'd'}, 60 {"verbose", no_argument, 0, 'v'}, 61 {"help", no_argument, 0, 'h'}, 62 {NULL} 63 }; 64 65 66 extern const char *__progname; 67 static const char *kProgramName = __progname; 68 69 static const char* kDefaultServerAddress = "freedb.freedb.org:80"; 70 static const char* kCddaFsName = "cdda"; 71 static const int kMaxTocSize = 1024; 72 73 74 CDDBLookup::CDDBLookup() 75 : 76 BApplication("application/x-vnd.Haiku-cddb_lookup") 77 { 78 } 79 80 81 CDDBLookup::~CDDBLookup() 82 { 83 } 84 85 86 void 87 CDDBLookup::LookupAll(CDDBServer& server, bool dumpOnly, bool verbose) 88 { 89 BVolumeRoster roster; 90 BVolume volume; 91 while (roster.GetNextVolume(&volume) == B_OK) { 92 Lookup(server, volume.Device(), dumpOnly, verbose); 93 } 94 } 95 96 97 status_t 98 CDDBLookup::Lookup(CDDBServer& server, const char* path, bool dumpOnly, 99 bool verbose) 100 { 101 BVolumeRoster roster; 102 BVolume volume; 103 while (roster.GetNextVolume(&volume) == B_OK) { 104 fs_info info; 105 if (fs_stat_dev(volume.Device(), &info) != B_OK) 106 continue; 107 108 if (strcmp(path, info.device_name) == 0) 109 return Lookup(server, volume.Device(), dumpOnly, verbose); 110 } 111 112 return B_ENTRY_NOT_FOUND; 113 } 114 115 116 status_t 117 CDDBLookup::Lookup(CDDBServer& server, const dev_t device, bool dumpOnly, 118 bool verbose) 119 { 120 scsi_toc_toc* toc = (scsi_toc_toc*)malloc(kMaxTocSize); 121 if (toc == NULL) 122 return B_NO_MEMORY; 123 124 uint32 cddbID; 125 if (!_ReadTOC(device, &cddbID, toc)) { 126 free(toc); 127 fprintf(stderr, "Skipping device with id %" B_PRId32 ".\n", device); 128 return B_BAD_TYPE; 129 } 130 131 printf("Looking up CD with CDDB Id %08" B_PRIx32 ".\n", cddbID); 132 133 BObjectList<QueryResponseData> queryResponses(10, true); 134 status_t result = server.Query(cddbID, toc, queryResponses); 135 if (result != B_OK) { 136 fprintf(stderr, "Error when querying CD: %s\n", strerror(result)); 137 free(toc); 138 return result; 139 } 140 141 free(toc); 142 143 const QueryResponseData* diskData = _SelectResult(queryResponses); 144 if (diskData == NULL) { 145 fprintf(stderr, "Could not find any CD entries in query response.\n"); 146 return B_BAD_INDEX; 147 } 148 149 ReadResponseData readResponse; 150 result = server.Read(*diskData, readResponse, verbose); 151 if (result != B_OK) { 152 fprintf(stderr, "Could not read detailed CD entry from server: %s\n", 153 strerror(result)); 154 return result; 155 } 156 157 if (dumpOnly) 158 _Dump(readResponse); 159 160 if (!dumpOnly) { 161 result = _WriteCDData(device, *diskData, readResponse); 162 if (result == B_OK) 163 printf("CD data saved.\n"); 164 else 165 fprintf(stderr, "Error writing CD data: %s\n", strerror(result)); 166 } 167 return B_OK; 168 } 169 170 171 status_t 172 CDDBLookup::Dump(CDDBServer& server, const char* category, const char* cddbID, 173 bool verbose) 174 { 175 ReadResponseData readResponse; 176 status_t status = server.Read(category, cddbID, "", readResponse, verbose); 177 if (status != B_OK) { 178 fprintf(stderr, "Could not read detailed CD entry from server: %s\n", 179 strerror(status)); 180 return status; 181 } 182 183 _Dump(readResponse); 184 return B_OK; 185 } 186 187 188 bool 189 CDDBLookup::_ReadTOC(const dev_t device, uint32* cddbID, 190 scsi_toc_toc* toc) const 191 { 192 if (cddbID == NULL || toc == NULL) 193 return false; 194 195 // Is it an Audio disk? 196 fs_info info; 197 fs_stat_dev(device, &info); 198 if (strncmp(info.fsh_name, kCddaFsName, strlen(kCddaFsName)) != 0) 199 return false; 200 201 // Does it have the CD:do_lookup attribute and is it true? 202 BVolume volume(device); 203 BDirectory directory; 204 volume.GetRootDirectory(&directory); 205 206 bool doLookup; 207 if (directory.ReadAttr("CD:do_lookup", B_BOOL_TYPE, 0, (void *)&doLookup, 208 sizeof(bool)) < B_OK || !doLookup) 209 return false; 210 211 // Does it have the CD:cddbid attribute? 212 if (directory.ReadAttr("CD:cddbid", B_UINT32_TYPE, 0, (void *)cddbID, 213 sizeof(uint32)) < B_OK) 214 return false; 215 216 // Does it have the CD:toc attribute? 217 if (directory.ReadAttr("CD:toc", B_RAW_TYPE, 0, (void *)toc, 218 kMaxTocSize) < B_OK) 219 return false; 220 221 return true; 222 } 223 224 225 const QueryResponseData* 226 CDDBLookup::_SelectResult(const QueryResponseList& responses) const 227 { 228 // Select a single CD match from the response and return it. 229 // 230 // TODO(bga):Right now it just picks the first entry on the list but 231 // someday we may want to let the user choose one. 232 int32 numItems = responses.CountItems(); 233 if (numItems > 0) { 234 if (numItems > 1) 235 printf("Multiple matches found :\n"); 236 237 for (int32 i = 0; i < numItems; i++) { 238 QueryResponseData* data = responses.ItemAt(i); 239 printf("* %s : %s - %s (%s)\n", data->cddbID.String(), 240 data->artist.String(), data->title.String(), 241 data->category.String()); 242 } 243 if (numItems > 1) 244 printf("Returning first entry.\n"); 245 246 return responses.ItemAt(0); 247 } 248 249 return NULL; 250 } 251 252 253 status_t 254 CDDBLookup::_WriteCDData(dev_t device, const QueryResponseData& diskData, 255 const ReadResponseData& readResponse) 256 { 257 // Rename volume. 258 BVolume volume(device); 259 260 status_t error = B_OK; 261 262 BString name = diskData.artist; 263 name += " - "; 264 name += diskData.title; 265 name.ReplaceSet("/", " "); 266 267 status_t result = volume.SetName(name.String()); 268 if (result != B_OK) { 269 printf("Can't set volume name.\n"); 270 return result; 271 } 272 273 // Rename tracks and add relevant Audio attributes. 274 BDirectory cddaRoot; 275 volume.GetRootDirectory(&cddaRoot); 276 277 BEntry entry; 278 int index = 0; 279 while (cddaRoot.GetNextEntry(&entry) == B_OK) { 280 TrackData* track = readResponse.tracks.ItemAt(index); 281 282 // Update name. 283 int trackNum = index + 1; // index=0 is actually Track 1 284 name.SetToFormat("%02d %s.wav", trackNum, track->title.String()); 285 name.ReplaceSet("/", " "); 286 287 result = entry.Rename(name.String()); 288 if (result != B_OK) { 289 fprintf(stderr, "%s: Failed renaming entry at index %d to " 290 "\"%s\".\n", kProgramName, index, name.String()); 291 error = result; 292 // User can benefit from continuing through all tracks. 293 // Report error later. 294 } 295 296 // Add relevant attributes. We consider an error here as non-fatal. 297 BNode node(&entry); 298 node.WriteAttrString("Media:Title", &track->title); 299 node.WriteAttrString("Audio:Album", &readResponse.title); 300 if (readResponse.genre.Length() != 0) 301 node.WriteAttrString("Media:Genre", &readResponse.genre); 302 if (readResponse.year != 0) { 303 node.WriteAttr("Media:Year", B_INT32_TYPE, 0, 304 &readResponse.year, sizeof(int32)); 305 } 306 307 if (track->artist == "") 308 node.WriteAttrString("Audio:Artist", &readResponse.artist); 309 else 310 node.WriteAttrString("Audio:Artist", &track->artist); 311 312 index++; 313 } 314 315 return error; 316 } 317 318 319 void 320 CDDBLookup::_Dump(const ReadResponseData& readResponse) const 321 { 322 printf("Artist: %s\n", readResponse.artist.String()); 323 printf("Title: %s\n", readResponse.title.String()); 324 printf("Genre: %s\n", readResponse.genre.String()); 325 printf("Year: %" B_PRIu32 "\n", readResponse.year); 326 puts("Tracks:"); 327 for (int32 i = 0; i < readResponse.tracks.CountItems(); i++) { 328 TrackData* track = readResponse.tracks.ItemAt(i); 329 if (track->artist.IsEmpty()) { 330 printf(" %2" B_PRIu32 ". %s\n", track->trackNumber + 1, 331 track->title.String()); 332 } else { 333 printf(" %2" B_PRIu32 ". %s - %s\n", track->trackNumber + 1, 334 track->artist.String(), track->title.String()); 335 } 336 } 337 } 338 339 340 // #pragma mark - 341 342 343 static void 344 usage(int exitCode) 345 { 346 fprintf(exitCode == EXIT_SUCCESS ? stdout : stderr, 347 "Usage: %s [-vdh] [-s <server>] [-i <category> <cddb-id>|<device>]\n" 348 "\nYou can specify the device either as path on the device, or using " 349 "the\ndevice name directly. If you do not specify a device, and are\n" 350 "using the -i option, all volumes will be scanned for CD info.\n\n" 351 " -s, --server\tUse alternative server. Default is %s.\n" 352 " -v, --verbose\tVerbose output.\n" 353 " -d, --dump\tDo not write attributes, only dump info to terminal.\n" 354 " -h, --help\tThis help text.\n" 355 " -i\t\tDump info for the specified category/cddb ID pair.\n", 356 kProgramName, kDefaultServerAddress); 357 exit(exitCode); 358 } 359 360 361 int 362 main(int argc, char* const* argv) 363 { 364 const char* serverAddress = kDefaultServerAddress; 365 const char* category = NULL; 366 bool verbose = false; 367 bool dump = false; 368 369 int c; 370 while ((c = getopt_long(argc, argv, "i:s:vdh", kLongOptions, NULL)) != -1) { 371 switch (c) { 372 case 0: 373 break; 374 case 'i': 375 category = optarg; 376 break; 377 case 's': 378 serverAddress = optarg; 379 break; 380 case 'v': 381 verbose = true; 382 break; 383 case 'd': 384 dump = true; 385 break; 386 case 'h': 387 usage(0); 388 break; 389 default: 390 usage(1); 391 break; 392 } 393 } 394 395 CDDBServer server(serverAddress); 396 CDDBLookup cddb; 397 int left = argc - optind; 398 399 if (category != NULL) { 400 if (left != 1) { 401 fprintf(stderr, "CDDB disc ID expected!\n"); 402 return EXIT_FAILURE; 403 } 404 405 const char* cddbID = argv[optind]; 406 cddb.Dump(server, category, cddbID, verbose); 407 } else { 408 // Lookup via actual CD 409 if (left > 0) { 410 for (int i = optind; i < argc; i++) { 411 // Allow to specify a device 412 const char* path = argv[i]; 413 status_t status; 414 if (strncmp(path, "/dev/", 5) == 0) { 415 status = cddb.Lookup(server, path, dump, verbose); 416 } else { 417 dev_t device = dev_for_path(path); 418 if (device >= 0) 419 status = cddb.Lookup(server, device, dump, verbose); 420 else 421 status = (status_t)device; 422 } 423 424 if (status != B_OK) { 425 fprintf(stderr, "Invalid path \"%s\": %s\n", path, 426 strerror(status)); 427 return EXIT_FAILURE; 428 } 429 } 430 } else 431 cddb.LookupAll(server, dump, verbose); 432 } 433 434 return 0; 435 } 436