1// Copyright 1992-2000, Be Incorporated, All Rights Reserved.
2// This file may be used under the terms of the Be Sample Code License.
3//
4// send comments/suggestions/feedback to pavel@be.com
5//
6
7#include "CDDBSupport.h"
8
9#include <Alert.h>
10#include <Debug.h>
11#include <Directory.h>
12#include <Entry.h>
13#include <File.h>
14#include <FindDirectory.h>
15#include <fs_attr.h>
16#include <fs_index.h>
17#include <NetAddress.h>
18#include <Path.h>
19#include <Query.h>
20#include <UTF8.h>
21#include <Volume.h>
22#include <VolumeRoster.h>
23
24#include <errno.h>
25#include <netdb.h>
26#include <signal.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <stdlib.h>
30
31
32//#define DEBUG_CDDB
33
34#ifdef DEBUG_CDDB
35#define STRACE(x) printf x
36#else
37#define STRACE(x) /* nothing */
38#endif
39
40const int kTerminatingSignal = SIGINT; // SIGCONT;
41
42// The SCSI table of contents consists of a 4-byte header followed by 100 track
43// descriptors, which are each 8 bytes. We don't really need the first 5 bytes
44// of the track descriptor, so we'll just ignore them. All we really want is the
45// length of each track, which happen to be the last 3 bytes of the descriptor.
46struct TrackRecord {
47	int8	unused[5];
48
49	int8	min;
50	int8	sec;
51	int8	frame;
52};
53
54
55static void
56DoNothing(int)
57{
58}
59
60
61static int32
62cddb_sum(int n)
63{
64	char buf[12];
65	int32 ret = 0;
66
67	sprintf(buf, "%u", n);
68	for (const char *p = buf; *p != '\0'; p++)
69		ret += (*p - '0');
70	return ret;
71}
72
73
74BString
75GetLineFromString(const char *string)
76{
77	if (!string)
78		return NULL;
79
80	BString out(string);
81
82	int32 lineFeed = out.FindFirst("\n");
83
84	if (lineFeed > 0)
85		out.Truncate(lineFeed);
86
87	return out;
88}
89
90
91//	#pragma mark -
92
93
94CDDBData::CDDBData(int32 discId)
95	:
96	fDiscID(discId),
97	fYear(-1)
98{
99}
100
101
102CDDBData::CDDBData(const CDDBData &from)
103	:
104	fDiscID(from.fDiscID),
105 	fArtist(from.fArtist),
106 	fAlbum(from.fAlbum),
107 	fGenre(from.fGenre),
108 	fDiscTime(from.fDiscTime),
109	fYear(from.fYear)
110{
111	STRACE(("CDDBData::Copy Constructor\n"));
112
113	for (int32 i = 0; i < from.fTrackList.CountItems(); i++) {
114		BString *string = (BString*)from.fTrackList.ItemAt(i);
115		CDAudioTime *time = (CDAudioTime*)from.fTimeList.ItemAt(i);
116
117		if (!string || !time)
118			continue;
119
120		AddTrack(string->String(), *time);
121	}
122}
123
124
125CDDBData::~CDDBData()
126{
127	STRACE(("CDDBData::~CDDBData\n"));
128	_EmptyLists();
129}
130
131
132CDDBData &
133CDDBData::operator=(const CDDBData &from)
134{
135	_EmptyLists();
136	fDiscID = from.fDiscID;
137 	fArtist = from.fArtist;
138 	fAlbum = from.fAlbum;
139 	fGenre = from.fGenre;
140 	fDiscTime = from.fDiscTime;
141	fYear = from.fYear;
142
143	for (int32 i = 0; i < from.fTrackList.CountItems(); i++) {
144		BString *string = (BString*)from.fTrackList.ItemAt(i);
145		CDAudioTime *time = (CDAudioTime*)from.fTimeList.ItemAt(i);
146
147		if (!string || !time)
148			continue;
149
150		AddTrack(string->String(),*time);
151	}
152	return *this;
153}
154
155
156void
157CDDBData::MakeEmpty()
158{
159	STRACE(("CDDBData::MakeEmpty\n"));
160
161	_EmptyLists();
162	fDiscID = -1;
163	fArtist = "";
164	fAlbum = "";
165	fGenre = "";
166	fDiscTime.SetMinutes(-1);
167	fDiscTime.SetSeconds(-1);
168	fYear = -1;
169}
170
171
172void
173CDDBData::_EmptyLists()
174{
175	STRACE(("CDDBData::_EmptyLists\n"));
176
177	for (int32 i = 0; i < fTrackList.CountItems(); i++) {
178		BString *string = (BString*)fTrackList.ItemAt(i);
179		delete string;
180	}
181	fTrackList.MakeEmpty();
182
183	for (int32 j = 0; j < fTimeList.CountItems(); j++) {
184		CDAudioTime *time = (CDAudioTime*)fTimeList.ItemAt(j);
185		delete time;
186	}
187	fTimeList.MakeEmpty();
188}
189
190
191status_t
192CDDBData::Load(const entry_ref &ref)
193{
194	STRACE(("CDDBData::Load(%s)\n", ref.name));
195
196	BFile file(&ref, B_READ_ONLY);
197
198	if (file.InitCheck() != B_OK) {
199		STRACE(("CDDBData::Load failed\n"));
200		return file.InitCheck();
201	}
202
203	attr_info info;
204
205	if (file.GetAttrInfo("Audio:Genre", &info) == B_OK && info.size > 0) {
206		char genreData[info.size + 2];
207
208		if (file.ReadAttr("Audio:Genre", B_STRING_TYPE, 0, genreData, info.size) > 0)
209			fGenre = genreData;
210	}
211
212	if (file.GetAttrInfo("Audio:Year", &info) == B_OK && info.size > 0) {
213		int32 yearData;
214
215		if (file.ReadAttr("Audio:Year", B_INT32_TYPE, 0, &yearData, info.size) > 0)
216			fYear = yearData;
217	}
218
219	// TODO: Attempt reading the file before attempting to read the attributes
220	if (file.GetAttrInfo("CD:tracks", &info) == B_OK && info.size > 0) {
221		char trackData[info.size + 2];
222
223		if (file.ReadAttr("CD:tracks", B_STRING_TYPE, 0, trackData, info.size) > 0) {
224			trackData[info.size] = 0;
225			BString tmp = GetLineFromString(trackData);
226			char *index;
227
228			if (fTrackList.CountItems() > 0)
229				_EmptyLists();
230
231			fArtist = tmp;
232			fArtist.Truncate(fArtist.FindFirst(" - "));
233			STRACE(("CDDBData::Load: Artist set to %s\n", fArtist.String()));
234
235			fAlbum = tmp.String() + (tmp.FindFirst(" - ") + 3);
236			STRACE(("CDDBData::Load: Album set to %s\n", fAlbum.String()));
237
238			index = strchr(trackData, '\n') + 1;
239			while (*index) {
240				tmp = GetLineFromString(index);
241
242				if (tmp.CountChars() > 0) {
243					BString *newTrack = new BString(tmp);
244					CDAudioTime *time = new CDAudioTime;
245					time->SetMinutes(0);
246					time->SetSeconds(0);
247
248					STRACE(("CDDBData::Load: Adding Track %s (%" B_PRId32 ":%" B_PRId32 ")\n", newTrack->String(),
249							time->minutes, time->seconds));
250
251					fTrackList.AddItem(newTrack);
252					fTimeList.AddItem(time);
253				}
254
255				index = strchr(index,'\n') + 1;
256			}
257
258			// We return this so that the caller knows to initialize tracktimes
259			return B_NO_INIT;
260		}
261	}
262
263	return B_ERROR;
264}
265
266
267status_t
268CDDBData::Load()
269{
270	// This uses the default R5 path
271
272	BPath path;
273	if (find_directory(B_USER_DIRECTORY, &path, true) != B_OK)
274		return B_ERROR;
275
276	path.Append("cd");
277	create_directory(path.Path(), 0755);
278
279	BString filename(path.Path());
280	filename << "/" << Artist() << " - " << Album();
281
282	if (filename.Compare("Artist") == 0)
283		filename << "." << DiscID();
284
285	BEntry entry(filename.String());
286	if (entry.InitCheck() != B_OK)
287		return entry.InitCheck();
288
289	entry_ref ref;
290	entry.GetRef(&ref);
291
292	return Load(ref);
293}
294
295
296status_t
297CDDBData::Save()
298{
299	// This uses the default R5 path
300
301	BPath path;
302	if (find_directory(B_USER_DIRECTORY, &path, true) != B_OK)
303		return B_ERROR;
304
305	path.Append("cd");
306	create_directory(path.Path(), 0755);
307
308	BString filename(path.Path());
309	filename << "/" << Artist() << " - " << Album();
310
311	if (filename.Compare("Artist")==0)
312		filename << "." << DiscID();
313
314	return Save(filename.String());
315}
316
317
318status_t
319CDDBData::Save(const char *filename)
320{
321	if (!filename) {
322		STRACE(("CDDBData::Save failed - NULL filename\n"));
323		return B_ERROR;
324	}
325
326	BFile file(filename, B_READ_WRITE | B_CREATE_FILE | B_ERASE_FILE);
327	if (file.InitCheck() != B_OK) {
328		STRACE(("CDDBData::Save failed - couldn't create file %s\n", filename));
329		return file.InitCheck();
330	}
331
332	BString entry;
333	char timestring[10];
334
335	sprintf(timestring,"%.2" B_PRId32 ":%.2" B_PRId32 "", fDiscTime.GetMinutes(),
336		fDiscTime.GetSeconds());
337
338	entry << fArtist << " - " << fAlbum << "\t" << timestring << "\n";
339	file.Write(entry.String(), entry.Length());
340
341	STRACE(("CDDBData::Save: wrote first line: %s", entry.String()));
342
343	BString tracksattr(fArtist);
344	tracksattr << " - " << fAlbum << "\n";
345
346	for (int32 i = 0; i < fTrackList.CountItems(); i++) {
347		BString *trackstr = (BString *)fTrackList.ItemAt(i);
348		CDAudioTime *time= (CDAudioTime*)fTimeList.ItemAt(i);
349
350		if (!trackstr || !time)
351			continue;
352
353		entry = *trackstr;
354
355		sprintf(timestring,"%.2" B_PRId32 ":%.2" B_PRId32 "", time->GetMinutes(),
356			time->GetSeconds());
357
358		entry << "\t" << timestring << "\n";
359		file.Write(entry.String(), entry.Length());
360		STRACE(("CDDBData::Save: Wrote line: %s", entry.String()));
361
362		tracksattr << *trackstr << "\n";
363	}
364
365	file.WriteAttr("CD:key", B_INT32_TYPE, 0, &fDiscID, sizeof(int32));
366	STRACE(("CDDBData::Save: Wrote CD identifier: %" B_PRId32 "(%" B_PRIx32 ")\n",
367		fDiscID, fDiscID));
368	file.WriteAttr("CD:tracks", B_STRING_TYPE, 0, tracksattr.String(), tracksattr.Length() + 1);
369
370	if (fGenre.CountChars() > 0)
371		file.WriteAttr("Audio:Genre",B_STRING_TYPE, 0, fGenre.String(), fGenre.Length() + 1);
372
373	if (fYear > 0)
374		file.WriteAttr("Audio:Year", B_INT32_TYPE, 0, &fYear, sizeof(int32));
375
376	return B_OK;
377}
378
379
380uint16
381CDDBData::CountTracks() const
382{
383	return fTrackList.CountItems();
384}
385
386
387bool
388CDDBData::RenameTrack(const int32 &index, const char *newName)
389{
390	if (!newName) {
391		STRACE(("CDDBData::RenameTrack failed - NULL newName\n"));
392		return false;
393	}
394
395	BString *name = (BString*)fTrackList.ItemAt(index);
396	if (name) {
397		STRACE(("CDDBData::RenameTrack(%" B_PRId32 ",%s)\n", index, newName));
398		name->SetTo(newName);
399		return true;
400	}
401
402	STRACE(("CDDBData::RenameTrack failed - invalid index\n"));
403	return false;
404}
405
406
407void
408CDDBData::AddTrack(const char *track, const CDAudioTime &time,
409	const int16 &index)
410{
411	if (!track) {
412		STRACE(("CDDBData::AddTrack failed - NULL name\n"));
413		return;
414	}
415	STRACE(("CDDBData::AddTrack(%s, %" B_PRId32 ":%.2" B_PRId32 ",%d)\n", track,
416		time.minutes, time.seconds, index));
417
418	fTrackList.AddItem(new BString(track));
419	fTimeList.AddItem(new CDAudioTime(time));
420}
421
422
423void
424CDDBData::RemoveTrack(const int16 &index)
425{
426	// This breaks the general following of the BList style because
427	// removing a track would require returning 2 pointers
428
429	fTrackList.RemoveItem(index);
430	fTimeList.RemoveItem(index);
431
432	STRACE(("CDDBData::RemoveTrack(%d)\n", index));
433}
434
435
436const char *
437CDDBData::TrackAt(const int16 &index) const
438{
439	BString *track = (BString*)fTrackList.ItemAt(index);
440	if (!track)
441		return NULL;
442
443	return track->String();
444}
445
446
447CDAudioTime *
448CDDBData::TrackTimeAt(const int16 &index)
449{
450	return (CDAudioTime *)fTimeList.ItemAt(index);
451}
452
453
454//	#pragma mark -
455
456
457CDDBQuery::CDDBQuery(const char *server, int32 port)
458	:
459	fServerName(server),
460	fPort(port),
461	fConnected(false),
462	fState(kInitial)
463{
464}
465
466
467CDDBQuery::~CDDBQuery()
468{
469	kill_thread(fThread);
470}
471
472
473void
474CDDBQuery::SetToSite(const char *server, int32 port)
475{
476	_Disconnect();
477	fServerName = server;
478	fPort = port;
479	fState = kInitial;
480}
481
482
483void
484CDDBQuery::SetToCD(const char *path)
485{
486	if (!path)
487		return;
488
489	// Get the SCSI table of contents from the device passed to us
490	int device = open(path, O_RDONLY);
491	if (device < 0)
492		return;
493
494	status_t result = ioctl(device, B_SCSI_GET_TOC, &fSCSIData);
495
496	close(device);
497
498	if (result != B_OK)
499		return;
500
501	// Calculate the disc's CDDB ID
502	if (fState == kInitial) {
503		fCDData.SetDiscID(GetDiscID(&fSCSIData));
504		fCDData.SetDiscTime(GetDiscTime(&fSCSIData));
505	} else {
506		int32 discID = GetDiscID(&fSCSIData);
507
508		if (fCDData.DiscID() == discID)
509			return;
510
511		fCDData.SetDiscID(discID);
512		fCDData.SetDiscTime(GetDiscTime(&fSCSIData));
513		fFrameOffsetString = OffsetsToString(&fSCSIData);
514	}
515
516	result = B_OK;
517	fState = kReading;
518	fThread = spawn_thread(&CDDBQuery::_QueryThread, "CDDBLookup", B_NORMAL_PRIORITY, this);
519
520	if (fThread >= 0)
521		resume_thread(fThread);
522	else {
523		fState = kError;
524		result = fThread;
525	}
526}
527
528
529const char *
530CDDBQuery::_GetToken(const char *stream, BString &result)
531{
532	result = "";
533	while (*stream && *stream <= ' ')
534		stream++;
535	while (*stream && *stream > ' ')
536		result += *stream++;
537
538	return stream;
539}
540
541
542status_t
543CDDBQuery::GetSites(bool (*eachFunc)(const char *site, int port, const char *latitude,
544		const char *longitude, const char *description, void *state), void *passThru)
545{
546	if (!_IsConnected())
547		_Connect();
548
549	BString tmp;
550
551	tmp = "sites\n";
552	STRACE((">%s", tmp.String()));
553
554	if (fSocket.Send(tmp.String(), tmp.Length()) == -1)
555		return B_ERROR;
556
557	_ReadLine(tmp);
558
559	if (tmp.FindFirst("210") == -1) {
560		_Disconnect();
561		return B_ERROR;
562	}
563
564	for (;;) {
565		BString site;
566		int32 sitePort;
567		BString latitude;
568		BString longitude;
569		BString description;
570
571		_ReadLine(tmp);
572		if (tmp == ".")
573			break;
574		const char *scanner = tmp.String();
575
576		scanner = _GetToken(scanner, site);
577		BString portString;
578		scanner = _GetToken(scanner, portString);
579		sitePort = atoi(portString.String());
580		scanner = _GetToken(scanner, latitude);
581		scanner = _GetToken(scanner, longitude);
582		description = scanner;
583
584		if (eachFunc(site.String(), sitePort, latitude.String(),
585				longitude.String(),	description.String(), passThru))
586			break;
587	}
588	_Disconnect();
589	return B_OK;
590}
591
592
593bool
594CDDBQuery::GetData(CDDBData *data, bigtime_t timeout)
595{
596	if (!data)
597		return false;
598
599	bigtime_t deadline = system_time() + timeout;
600	while (fState == kReading) {
601		snooze(50000);
602		if (system_time() > deadline)
603			break;
604	}
605	if (fState != kDone)
606		return false;
607
608	*data = fCDData;
609
610	return true;
611}
612
613
614int32
615CDDBQuery::GetDiscID(const scsi_toc *toc)
616{
617	// The SCSI TOC data has a 4-byte header and then each track record starts
618	TrackRecord *tocData = (TrackRecord*)&(toc->toc_data[4]);
619
620	int32 numTracks = toc->toc_data[3] - toc->toc_data[2] + 1;
621
622	int32 sum1 = 0;
623	int32 sum2 = 0;
624	for (int index = 0; index < numTracks; index++) {
625		sum1 += cddb_sum((tocData[index].min * 60) + tocData[index].sec);
626
627		// the following is probably running over too far
628		sum2 +=	(tocData[index + 1].min * 60 + tocData[index + 1].sec)
629				- (tocData[index].min * 60 + tocData[index].sec);
630	}
631	int32 discID = ((sum1 % 0xff) << 24) + (sum2 << 8) + numTracks;
632
633	return discID;
634}
635
636
637BString
638CDDBQuery::OffsetsToString(const scsi_toc *toc)
639{
640	TrackRecord *tocData = (TrackRecord*)&(toc->toc_data[4]);
641
642	int32 numTracks = toc->toc_data[3] - toc->toc_data[2] + 1;
643
644	BString string;
645	for (int index = 0; index < numTracks; index++) {
646		string << tocData[index].min * 4500 + tocData[index].sec * 75
647			+ tocData[index].frame << ' ';
648	}
649
650	return string;
651}
652
653
654int32
655CDDBQuery::GetTrackCount(const scsi_toc *toc)
656{
657	return toc->toc_data[3] - toc->toc_data[2] + 1;
658}
659
660
661CDAudioTime
662CDDBQuery::GetDiscTime(const scsi_toc *toc)
663{
664	int16 trackcount = toc->toc_data[3] - toc->toc_data[2] + 1;
665	TrackRecord *desc = (TrackRecord*)&(toc->toc_data[4]);
666
667	CDAudioTime disc;
668	disc.SetMinutes(desc[trackcount].min);
669	disc.SetSeconds(desc[trackcount].sec);
670
671	return disc;
672}
673
674
675void
676CDDBQuery::GetTrackTimes(const scsi_toc *toc, vector<CDAudioTime> &times)
677{
678	TrackRecord *tocData = (TrackRecord*)&(toc->toc_data[4]);
679	int16 trackCount = toc->toc_data[3] - toc->toc_data[2] + 1;
680
681	for (int index = 0; index < trackCount + 1; index++) {
682		CDAudioTime cdtime;
683		cdtime.SetMinutes(tocData[index].min);
684		cdtime.SetSeconds(tocData[index].sec);
685		times.push_back(cdtime);
686	}
687}
688
689
690status_t
691CDDBQuery::_ReadFromServer(BString &data)
692{
693	// This function queries the given CDDB server for the existence of the
694	// disc's data and saves the data to file once obtained.
695
696	// Query for the existence of the disc in the database
697	char idString[10];
698	sprintf(idString, "%08" B_PRIx32, fCDData.DiscID());
699	BString query;
700
701	int32 trackCount = GetTrackCount(&fSCSIData);
702	BString offsetString = OffsetsToString(&fSCSIData);
703	int32 discLength = (fCDData.DiscTime()->GetMinutes() * 60)
704		+ fCDData.DiscTime()->GetSeconds();
705
706	query << "cddb query " << idString << ' ' << trackCount << ' '
707			<< offsetString << ' ' << discLength << '\n';
708
709	STRACE((">%s", query.String()));
710
711	if (fSocket.Send(query.String(), query.Length()) == -1) {
712		_Disconnect();
713		return B_ERROR;
714	}
715
716	BString tmp;
717	_ReadLine(tmp);
718	STRACE(("<%s", tmp.String()));
719
720	BString category;
721	BString queryDiscID(idString);
722
723	if (tmp.FindFirst("200") != 0) {
724		if (tmp.FindFirst("211") == 0) {
725			// A 211 means that the query was not exact. To make sure that we
726			// don't have a problem with this in the future, we will choose the
727			// first entry that the server returns. This may or may not be wise,
728			// but in my experience, the first one has been the right one.
729
730			_ReadLine(tmp);
731
732			// Get the category from the what the server returned
733			_GetToken(tmp.String(), category);
734
735			// Now we will get the disc ID for the CD. We will need this when we
736			// query for the track name list. If we send the track name query
737			// with the real discID, nothing will be returned. However, if we
738			// send the one from the entry, we'll get the names and we can take
739			// these names attach them to the disc that we have.
740			_GetToken(tmp.String() + category.CountChars(), queryDiscID);
741
742			// This is to suck up any more search results that the server sends
743			// us.
744			BString throwaway;
745			_ReadLine(throwaway);
746			while (throwaway.ByteAt(0) != '.')
747				_ReadLine(throwaway);
748		} else {
749			// We get here for any time the CDDB server does not recognize the
750			// CD, amongst other things
751			STRACE(("CDDB lookup error: %s\n", tmp.String()));
752			fCDData.SetGenre("misc");
753			return B_NAME_NOT_FOUND;
754		}
755	} else {
756		_GetToken(tmp.String() + 3, category);
757	}
758	STRACE(("CDDBQuery::_ReadFromServer: Genre: %s\n", category.String()));
759	if (!category.Length()) {
760		STRACE(("Set genre to 'misc'\n"));
761		category = "misc";
762	}
763
764	// Query for the disc's data - artist, album, tracks, etc.
765	query = "";
766	query << "cddb read " << category << ' ' << queryDiscID << '\n' ;
767	if (fSocket.Send(query.String(), query.Length()) == -1) {
768		_Disconnect();
769		return B_ERROR;
770	}
771
772	while (true) {
773		_ReadLine(tmp);
774
775		if (tmp == "." || tmp == ".\n")
776			break;
777
778		data << tmp << '\n';
779	}
780
781	return B_OK;
782}
783
784
785status_t
786CDDBQuery::_Connect()
787{
788	if (fConnected)
789		_Disconnect();
790
791	BNetAddress address;
792
793	status_t status = address.SetTo(fServerName.String(), fPort);
794	if (status != B_OK)
795		return status;
796
797	status = fSocket.Connect(address);
798	if (status != B_OK)
799		return status;
800
801	fConnected = true;
802
803	BString tmp;
804	_ReadLine(tmp);
805	_IdentifySelf();
806
807	return B_OK;
808}
809
810
811bool
812CDDBQuery::_IsConnected() const
813{
814	return fConnected;
815}
816
817
818void
819CDDBQuery::_Disconnect()
820{
821	if (fConnected) {
822		fSocket.Close();
823		fConnected = false;
824	}
825}
826
827
828void
829CDDBQuery::_ReadLine(BString &buffer)
830{
831	buffer = "";
832	unsigned char ch;
833
834	for (;;) {
835		if (fSocket.Receive(&ch, 1) <= 0)
836			break;
837
838
839		// This function is more work than it should have to be. FreeDB lookups
840		// can sometimes  be in a non-ASCII encoding, such as Latin-1 or UTF8.
841		// The problem lies in Be's implementation of BString, which does not
842		// support UTF8 string assignments. The Be Book says we have to flatten
843		// the string and adjust the character counts manually. Man, this
844		// *really* sucks.
845		if (ch > 0x7f) 	{
846			// Obviously non-ASCII character detected. Let's see if it's Latin-1
847			// or UTF8.
848			unsigned char *string, *stringindex;
849			int32 length = buffer.Length();
850
851			// The first byte of a UTF8 string will be 110xxxxx
852			if ( ((ch & 0xe0) == 0xc0)) {
853				// This is UTF8. Get the next byte
854				unsigned char ch2;
855				if (fSocket.Receive(&ch2, 1) <= 0)
856					break;
857
858				if ( (ch2 & 0xc0) == 0x80) {
859					string = (unsigned char *)buffer.LockBuffer(length + 10);
860					stringindex = string + length;
861
862					stringindex[0] = ch;
863					stringindex[1] = ch2;
864					stringindex[2] = 0;
865
866					buffer.UnlockBuffer();
867
868					// We've added the character, so go to the next iteration
869					continue;
870				}
871			}
872
873			// Nope. Just Latin-1. Convert to UTF8 and assign
874			char srcstr[2], deststr[5];
875			int32 srclen, destlen, state;
876
877			srcstr[0] = ch;
878			srcstr[1] = '\0';
879			srclen = 1;
880			destlen = 5;
881			memset(deststr, 0, 5);
882
883			if (convert_to_utf8(B_ISO1_CONVERSION, srcstr, &srclen, deststr,
884					&destlen, &state) == B_OK) {
885				// We succeeded. Amazing. Now we hack the string into having the
886				// character
887				length = buffer.Length();
888
889				string = (unsigned char *)buffer.LockBuffer(length + 10);
890				stringindex = string + length;
891
892				for (int i = 0; i < 5; i++) {
893					stringindex[i] = deststr[i];
894					if (!deststr[i])
895						break;
896				}
897
898				buffer.UnlockBuffer();
899			} else {
900				// well, we tried. Append the character to the string and live
901				// with it
902				buffer += ch;
903			}
904		} else
905			buffer += ch;
906
907		if (ch == '\n')
908			break;
909	}
910
911	buffer.RemoveAll("\r");
912	STRACE(("<%s", buffer.String()));
913}
914
915
916status_t
917CDDBQuery::_IdentifySelf()
918{
919	BString username, hostname;
920
921	username = getenv("USER");
922	hostname = getenv("HOSTNAME");
923
924	if (username.Length() < 1)
925		username = "baron";
926
927	if (hostname.Length() < 1)
928		hostname = "haiku";
929
930	BString tmp;
931	tmp << "cddb hello " << username << " " << hostname
932		<< " HaikuCDPlayer 1.0\n";
933
934	STRACE((">%s", tmp.String()));
935	if (fSocket.Send(tmp.String(), tmp.Length())==-1) {
936		_Disconnect();
937		return B_ERROR;
938	}
939
940	_ReadLine(tmp);
941
942	return B_OK;
943}
944
945
946status_t
947CDDBQuery::_OpenContentFile(const int32 &discID)
948{
949	// Makes sure that the lookup has a valid file to work with for the CD
950	// content. Returns true if there is an existing file, false if a lookup is
951	// required.
952
953	BFile file;
954	BString predicate;
955	predicate << "CD:key == " << discID;
956	entry_ref ref;
957
958	BVolumeRoster roster;
959	BVolume volume;
960	roster.Rewind();
961	while (roster.GetNextVolume(&volume) == B_OK) {
962		if (volume.IsReadOnly() || !volume.IsPersistent() || !volume.KnowsAttr()
963			|| !volume.KnowsQuery())
964			continue;
965
966		// make sure the volume we are looking at is indexed right
967		fs_create_index(volume.Device(), "CD:key", B_INT32_TYPE, 0);
968
969		BQuery query;
970		query.SetVolume(&volume);
971		query.SetPredicate(predicate.String());
972		if (query.Fetch() != B_OK)
973			continue;
974
975		if (query.GetNextRef(&ref) == B_OK)
976			break;
977	}
978
979	status_t status = fCDData.Load(ref);
980	if (status == B_NO_INIT) {
981		// We receive this error when the Load() function couldn't load the
982		// track times This just means that we get it from the SCSI data given
983		// to us in SetToCD
984		vector<CDAudioTime> times;
985		GetTrackTimes(&fSCSIData,times);
986
987		for (int32 i = 0; i < fCDData.CountTracks(); i++) {
988			CDAudioTime *item = fCDData.TrackTimeAt(i);
989			*item = times[i + 1] - times[i];
990		}
991
992		status = B_OK;
993	}
994
995	return status;
996}
997
998
999int32
1000CDDBQuery::_QueryThread(void *owner)
1001{
1002	CDDBQuery *query = (CDDBQuery *)owner;
1003
1004	signal(kTerminatingSignal, DoNothing);
1005
1006	if (query->_OpenContentFile(query->fCDData.DiscID()) != B_OK) {
1007		// new content file, read it in from the server
1008		if (query->_Connect() == B_OK) {
1009			BString data;
1010
1011			query->_ReadFromServer(data);
1012			query->_ParseData(data);
1013			query->_WriteFile();
1014		} else {
1015			// We apparently couldn't connect to the server, so we'll need to
1016			// handle creating tracknames. Note that we do not save to disk.
1017			// This is because it should be up to the user what to do.
1018			query->_SetDefaultInfo();
1019		}
1020	}
1021
1022	query->fState = kDone;
1023	query->fThread = -1;
1024	query->fResult = B_OK;
1025
1026	return 0;
1027}
1028
1029
1030void
1031CDDBQuery::_WriteFile()
1032{
1033	BPath path;
1034	if (find_directory(B_USER_DIRECTORY, &path, true) != B_OK)
1035		return;
1036
1037	path.Append("cd");
1038	create_directory(path.Path(), 0755);
1039
1040	BString filename(path.Path());
1041	filename << "/" << fCDData.Artist() << " - " << fCDData.Album();
1042
1043	if (filename.Compare("Artist") == 0)
1044		filename << "." << fCDData.DiscID();
1045
1046	fCDData.Save(filename.String());
1047}
1048
1049
1050void
1051CDDBQuery::_SetDefaultInfo()
1052{
1053	for (int16 i = fCDData.CountTracks(); i >= 0; i--)
1054		fCDData.RemoveTrack(i);
1055
1056	vector<CDAudioTime> trackTimes;
1057	GetTrackTimes(&fSCSIData, trackTimes);
1058	int32 trackCount = GetTrackCount(&fSCSIData);
1059
1060	fCDData.SetArtist("Artist");
1061	fCDData.SetAlbum("Audio CD");
1062	fCDData.SetGenre("Misc");
1063
1064	for (int32 i = 0; i < trackCount; i++) {
1065		BString trackname("Track ");
1066
1067		if (i < 9)
1068			trackname << "0";
1069
1070		trackname << i + 1;
1071
1072		CDAudioTime time = trackTimes[i + 1] - trackTimes[i];
1073		fCDData.AddTrack(trackname.String(), time);
1074	}
1075}
1076
1077
1078void
1079CDDBQuery::_ParseData(const BString &data)
1080{
1081	// Can't simply call MakeEmpty() because the thread is spawned when the
1082	// discID kept in fCDData is not the same as what's in the drive and
1083	// MakeEmpty invalidates *everything* in the object. Considering that we
1084	// reassign everything here, simply emptying the track list should be sufficient
1085	for (int16 i = fCDData.CountTracks(); i >= 0; i--)
1086		fCDData.RemoveTrack(i);
1087
1088	vector<CDAudioTime> trackTimes;
1089	GetTrackTimes(&fSCSIData,trackTimes);
1090	int32 trackCount = GetTrackCount(&fSCSIData);
1091
1092	if (data.CountChars() < 1) {
1093		// This case occurs when the CDDB lookup fails. On these occasions, we
1094		// need to generate the file ourselves. This is actually pretty easy.
1095		fCDData.SetArtist("Artist");
1096		fCDData.SetAlbum("Audio CD");
1097		fCDData.SetGenre("Misc");
1098
1099		for (int32 i = 0; i < trackCount; i++) 	{
1100			BString trackname("Track ");
1101
1102			if (i < 9)
1103				trackname << "0";
1104
1105			trackname << i + 1;
1106
1107			CDAudioTime time = trackTimes[i + 1] - trackTimes[i];
1108			fCDData.AddTrack(trackname.String(), time);
1109		}
1110	}
1111
1112	// TODO: This function is dog slow, but it works. Optimize.
1113
1114	// Ideally, the search should be done sequentially using GetLineFromString()
1115	// and strchr(). Order: genre(category), frame offsets, disc length,
1116	// artist/album, track titles
1117
1118	int32 pos;
1119
1120	pos = data.FindFirst("DYEAR=");
1121	if (pos > 0) {
1122		BString artist,album;
1123		artist = album = GetLineFromString(data.String() + sizeof("DYEAR")
1124			+ pos);
1125
1126		// TODO: finish, once I find an entry which actually has a year in it
1127		BAlert *alert = new BAlert("SimplyVorbis", "DYEAR entry found\n", "OK");
1128		alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
1129		alert->Go();
1130
1131	}
1132
1133	pos = data.FindFirst("DTITLE=");
1134	if (pos > 0) {
1135		BString artist,album;
1136		artist = album = GetLineFromString(data.String() + sizeof("DTITLE")
1137			+ pos);
1138
1139		pos = artist.FindFirst(" / ");
1140		if (pos > 0)
1141			artist.Truncate(pos);
1142		fCDData.SetArtist(artist.String());
1143
1144		album = album.String() + pos + sizeof(" / ") - 1;
1145		fCDData.SetAlbum(album.String());
1146	}
1147
1148	BString category = GetLineFromString(data.String() + 4);
1149	pos = category.FindFirst(" ");
1150	if (pos > 0)
1151		category.Truncate(pos);
1152	fCDData.SetGenre(category.String());
1153
1154	pos = data.FindFirst("TTITLE0=");
1155	if (pos > 0) {
1156		int32 trackCount = 0;
1157		BString searchString = "TTITLE0=";
1158
1159		while (pos > 0) {
1160			BString trackName = data.String() + pos + searchString.Length();
1161			trackName.Truncate(trackName.FindFirst("\n"));
1162
1163			CDAudioTime tracktime = trackTimes[trackCount + 1]
1164				- trackTimes[trackCount];
1165			fCDData.AddTrack(trackName.String(),tracktime);
1166
1167			trackCount++;
1168			searchString = "TTITLE";
1169			searchString << trackCount << "=";
1170			pos = data.FindFirst(searchString.String(),pos);
1171		}
1172	}
1173}
1174
1175
1176void CDDBQuery::SetData(const CDDBData &data)
1177{
1178	fCDData = data;
1179}
1180