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
29class CDDBLookup : public BApplication {
30public:
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
43private:
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
57static 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
66extern const char *__progname;
67static const char *kProgramName = __progname;
68
69static const char* kDefaultServerAddress = "freedb.freedb.org:80";
70static const char* kCddaFsName = "cdda";
71static const int kMaxTocSize = 1024;
72
73
74CDDBLookup::CDDBLookup()
75	:
76	BApplication("application/x-vnd.Haiku-cddb_lookup")
77{
78}
79
80
81CDDBLookup::~CDDBLookup()
82{
83}
84
85
86void
87CDDBLookup::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
97status_t
98CDDBLookup::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
116status_t
117CDDBLookup::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
171status_t
172CDDBLookup::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
188bool
189CDDBLookup::_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
225const QueryResponseData*
226CDDBLookup::_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
253status_t
254CDDBLookup::_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
319void
320CDDBLookup::_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
343static void
344usage(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
361int
362main(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