xref: /haiku/src/bin/cddb_lookup/cddb_lookup.cpp (revision e81a954787e50e56a7f06f72705b7859b6ab06d1)
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