1/* vim: set ft=objc ts=4 nowrap: */
2/*
3 *  TrackList.m
4 *
5 *  Copyright (c) 2003
6 *
7 *  Author: Andreas Schik <andreas@schik.de>
8 *
9 *  This program is free software; you can redistribute it and/or modify
10 *  it under the terms of the GNU General Public License as published by
11 *  the Free Software Foundation; either version 2 of the License, or
12 *  (at your option) any later version.
13 *
14 *  This program is distributed in the hope that it will be useful,
15 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 *  GNU General Public License for more details.
18 *
19 *  You should have received a copy of the GNU General Public License
20 *  along with this program; if not, write to the Free Software
21 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22 */
23
24
25#include <AppKit/AppKit.h>
26#include <Cddb/Cddb.h>
27#include "TrackList.h"
28
29static TrackList *sharedTrackList = nil;
30
31
32
33@implementation TrackList
34
35
36- (id) init
37{
38	[self initWithNibName: @"TrackList"];
39	return self;
40}
41
42
43- (id) initWithNibName: (NSString *) nibName;
44{
45	if (sharedTrackList) {
46		[self dealloc];
47	} else {
48		self = [super init];
49		if (![NSBundle loadNibNamed: nibName owner: self]) {
50			NSLog (@"Could not load nib \"%@\".", nibName);
51		} else {
52			sharedTrackList = self;
53
54			artist = [[NSString alloc] initWithString: _(@"Unknown")];
55			title = [[NSString alloc] initWithString: _(@"Unknown")];
56
57			[window setExcludedFromWindowsMenu: YES];
58			[titleField setStringValue: _(@"No CD")];
59
60            [window setFrameAutosaveName: @"CDTrackListWindow"];
61            [window setFrameUsingName: @"CDTrackListWindow"];
62		}
63	}
64	return sharedTrackList;
65}
66
67- (void) dealloc
68{
69	RELEASE(toc);
70	RELEASE(artist);
71	RELEASE(title);
72	[super dealloc];
73}
74
75- (void) activate
76{
77	[window makeKeyAndOrderFront: self];
78}
79
80- (BOOL) isVisible
81{
82	return [window isVisible];
83}
84
85- (void) setTOC: (NSDictionary *) newTOC
86{
87	ASSIGN(toc, newTOC);
88	DESTROY(artist);
89	DESTROY(title);
90
91	if (!toc) {
92		[titleField setStringValue: _(@"No CD")];
93	} else {
94		/*
95		 * Try to get locally cached cddb data for the CD.
96		 */
97		NSDictionary *cdInfo = [self getCddbResultFromCache: [toc objectForKey: @"cddbid"]];
98		if (cdInfo != nil) {
99			int i;
100			NSString *dspTitle;
101			NSArray *tracks;
102
103			ASSIGN(artist, [[cdInfo objectForKey: @"artists"] objectAtIndex: 0]);
104			ASSIGN(title, [cdInfo objectForKey: @"album"]);
105
106			dspTitle = [NSString stringWithFormat: @"%@ - %@", artist, title];
107
108			[titleField setStringValue: dspTitle];
109
110			tracks = [toc objectForKey: @"tracks"];
111
112			for (i = 0; i < [tracks count]; i++) {
113				[[tracks objectAtIndex: i] setObject: [[cdInfo objectForKey: @"titles"] objectAtIndex: i]
114											forKey: @"title"];
115				[[tracks objectAtIndex: i] setObject: [[cdInfo objectForKey: @"artists"] objectAtIndex: i]
116											forKey: @"artist"];
117			}
118		} else {
119			artist = [[NSString alloc] initWithString: _(@"Unknown")];
120			title = [[NSString alloc] initWithString: _(@"Unknown")];
121			[titleField setStringValue: [NSString stringWithFormat: @"%@: %@", _(@"CD"), [toc objectForKey: @"cddbid"]]];
122		}
123	}
124
125	[trackListView reloadData];
126	[[NSApp mainMenu] update];
127}
128
129- (void) setPlaysTrack: (int) track
130{
131	playsTrack = track;
132
133	[trackListView reloadData];
134}
135
136- (BOOL) validateMenuItem: (NSMenuItem*)item
137{
138	SEL	action = [item action];
139
140	// without a TOC (=> no CD) we are not going to query
141	// a FreeDB database
142	if (sel_isEqual(action, @selector(queryCddb:))) {
143		if (!toc)
144			return NO;
145	}
146	return YES;
147}
148
149
150- (NSString *)createCddbQuery: (NSDictionary *)theTOC
151{
152	int i;
153	NSArray *tracks;
154	NSMutableString *cddbQuery;
155
156	tracks = [theTOC objectForKey: @"tracks"];
157	cddbQuery = [NSMutableString stringWithFormat: @"%@ %d",
158							[theTOC objectForKey: @"cddbid"],
159							[[theTOC objectForKey: @"numberOfTracks"] intValue]];
160
161	for (i = 0; i < [tracks count]; i++) {
162		[cddbQuery appendFormat: @" %d", [[[tracks objectAtIndex: i] objectForKey: @"offset"] intValue]];
163	}
164
165	[cddbQuery appendFormat: @" %d", ([[theTOC objectForKey: @"discLength"] intValue] -
166						[[[tracks objectAtIndex: 0] objectForKey: @"offset"] intValue]) / 75];
167
168	return cddbQuery;
169}
170
171
172
173- (void)queryCddb:(id)sender
174{
175	NSString *cddbServer;
176	Cddb *cddb = nil;
177
178	if (!toc)
179		return;
180
181	cddbServer = [[NSUserDefaults standardUserDefaults] objectForKey: @"FreedbSite"];
182
183	if (cddbServer && [cddbServer length]) {
184		int i;
185		NSArray *searchPaths;
186		NSString *bundlePath;
187		NSBundle *bundle;
188		Class bundleClass;
189
190		// try to load the Cddb bundle
191		searchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
192													NSUserDomainMask|NSLocalDomainMask|NSSystemDomainMask, YES);
193
194		for (i = 0; i < [searchPaths count]; i++) {
195			bundlePath = [NSString stringWithFormat: @"%@/Bundles/Cddb.bundle", [searchPaths objectAtIndex: i]];
196
197			bundle = [NSBundle bundleWithPath: bundlePath];
198			if (bundle) {
199				bundleClass = [bundle principalClass];
200				if (bundleClass) {
201					cddb = [bundleClass new];
202					break;
203				} else {
204				}
205			} else {
206			}
207		}    // for (i = 0; i < [searchPaths count]; i++)
208	}    // if (cddbServer) {
209
210	if (cddb != nil) {
211		NSArray *matches;
212		NSDictionary *cdInfo;
213		NSString *queryString = [self createCddbQuery: toc];
214		NSArray *tracks;
215
216		[cddb setDefaultSite: cddbServer];
217		matches = [cddb query: queryString];
218		if ((matches != nil) && [matches count]) {
219			cdInfo = [cddb readWithCategory: [[matches objectAtIndex: 0] objectForKey: @"category"]
220							discid: [[matches objectAtIndex: 0] objectForKey: @"discid"]
221							postProcess: YES];
222
223			if (cdInfo != nil) {
224				int i;
225				NSString *dspTitle;
226
227				[self saveCddbResultInCache: [toc objectForKey: @"cddbid"] cdInfo: cdInfo];
228				ASSIGN(artist, [[cdInfo objectForKey: @"artists"] objectAtIndex: 0]);
229				ASSIGN(title, [cdInfo objectForKey: @"album"]);
230
231				dspTitle = [NSString stringWithFormat: @"%@ - %@", artist, title];
232
233				[titleField setStringValue: dspTitle];
234
235				tracks = [toc objectForKey: @"tracks"];
236
237				for (i = 0; i < [tracks count]; i++) {
238					[[tracks objectAtIndex: i] setObject: [[cdInfo objectForKey: @"titles"] objectAtIndex: i]
239												forKey: @"title"];
240					[[tracks objectAtIndex: i] setObject: [[cdInfo objectForKey: @"artists"] objectAtIndex: i]
241												forKey: @"artist"];
242				}
243				[trackListView reloadData];
244			} else {   // if (cdInfo != nil)
245				NSRunAlertPanel(@"CDPlayer",
246						_(@"Couldn't read CD information."),
247						_(@"OK"), nil, nil);
248			}
249		} else {
250			NSRunAlertPanel(@"CDPlayer",
251					_(@"Couldn't find any matches."),
252					_(@"OK"), nil, nil);
253		}
254	} else {    // if (cddb != nil)
255		NSRunAlertPanel(@"CDPlayer",
256				_(@"Couldn't find Cddb bundle."),
257				_(@"OK"), nil, nil);
258	}
259}
260
261- (void) saveCddbResultInCache: (NSString *) discid
262					   cdInfo: (NSDictionary *) cdInfo
263{
264	NSString *basePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
265	NSString *cacheDir = [basePath stringByAppendingPathComponent: @"CDPlayer"];
266	NSString *cacheFile;
267	NSFileManager *fm = [NSFileManager defaultManager];
268	BOOL isdir;
269
270	if (([fm fileExistsAtPath: cacheDir isDirectory: &isdir] & isdir) == NO) {
271		if ([fm createDirectoryAtPath: cacheDir attributes: nil] == NO) {
272			NSLog(@"unable to create: %@", cacheDir);
273			return;
274		}
275	}
276	cacheDir = [cacheDir stringByAppendingPathComponent: @"discinfo"];
277
278	if (([fm fileExistsAtPath: cacheDir isDirectory: &isdir] & isdir) == NO) {
279		if ([fm createDirectoryAtPath: cacheDir attributes: nil] == NO) {
280			NSLog(@"unable to create: %@", cacheDir);
281			return;
282		}
283	}
284	cacheFile = [cacheDir stringByAppendingPathComponent: discid];
285	[cdInfo writeToFile: cacheFile atomically: YES];
286}
287
288- (NSDictionary *) getCddbResultFromCache: (NSString *) discid
289{
290	NSDictionary *result = nil;
291	NSString *basePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
292	NSString *cacheDir = [basePath stringByAppendingPathComponent: @"CDPlayer"];
293	NSString *cacheFile;
294	NSFileManager *fm = [NSFileManager defaultManager];
295	BOOL isdir;
296
297	if (([fm fileExistsAtPath: cacheDir isDirectory: &isdir] & isdir) == NO) {
298		return nil;
299	}
300	cacheDir = [cacheDir stringByAppendingPathComponent: @"discinfo"];
301
302	if (([fm fileExistsAtPath: cacheDir isDirectory: &isdir] & isdir) == NO) {
303		return nil;
304	}
305	cacheFile = [cacheDir stringByAppendingPathComponent: discid];
306	result = [[NSDictionary alloc] initWithContentsOfFile: cacheFile];
307	return result;
308}
309
310- (int) numberOfTracksInTOC
311{
312	if (!toc)
313		return 0;
314	else
315		return [[toc objectForKey: @"numberOfTracks"] intValue];
316}
317
318
319//
320// NSTableView data source methods
321//
322- (int) numberOfRowsInTableView: (NSTableView *) tableView
323{
324	return [self numberOfTracksInTOC];
325}
326
327- (id) tableView: (NSTableView *) tableView
328	objectValueForTableColumn: (NSTableColumn *)tableColumn
329	row: (int)rowIndex
330{
331	NSString *identifier = [tableColumn identifier];
332	NSArray *tracks = [toc objectForKey: @"tracks"];
333	NSDictionary *track = nil;
334
335	if ([identifier isEqual: @"Nr"])
336		return [NSString stringWithFormat: @"%d", rowIndex+1];
337
338	track = [tracks objectAtIndex: rowIndex];
339	if ([identifier isEqual: @"Duration"]) {
340		long min, sec, frames;
341		long totalTime;		// Frames
342
343		totalTime = [[track objectForKey: @"length"] intValue];
344
345		frames = totalTime % 75;
346		sec = totalTime / 75;
347		min = sec / 60;
348		sec = sec % 60;
349
350		return [NSString stringWithFormat: @"%02d:%02d.%02d", min, sec, frames];
351	}
352	if ([identifier isEqual: @"Artist"]) {
353		return [track objectForKey: @"artist"];
354	}
355	if ([[track objectForKey: @"type"] isEqualToString: @"data"])
356		return [NSString stringWithFormat: _(@"%@ [Data]"),
357										[track objectForKey: @"title"]];
358	else
359		return [track objectForKey: @"title"];
360}
361
362- (void)  tableView: (NSTableView *)tableView
363	willDisplayCell: (id) cell
364	 forTableColumn: (NSTableColumn *) tableColumn
365				row: (int) rowIndex
366{
367	if (rowIndex == playsTrack-1)
368		[cell setFont: [NSFont boldSystemFontOfSize: 0]];
369	else
370		[cell setFont: [NSFont systemFontOfSize: 0]];
371}
372
373- (BOOL) tableView: (NSTableView *) tableView
374		 writeRows: (NSArray *) rows
375	  toPasteboard: (NSPasteboard *) pboard
376{
377	int i;
378	NSMutableDictionary *propertyList;
379	NSMutableDictionary *cdProperties;
380	NSMutableArray *tracks;
381
382	propertyList = [[NSMutableDictionary alloc] initWithCapacity: 1];
383	cdProperties = [[NSMutableDictionary alloc] initWithCapacity: 3];
384	tracks = [[NSMutableArray alloc] initWithCapacity: [rows count]];
385
386	[cdProperties setObject: artist forKey: @"artist"];
387	[cdProperties setObject: title forKey: @"title"];
388
389	for (i = 0; i < [rows count]; i++) {
390		int row;
391		id track;
392		NSMutableDictionary *addTrack = [NSMutableDictionary new];
393
394		row = [[rows objectAtIndex: i] intValue];
395		track = [[toc objectForKey: @"tracks"] objectAtIndex: row];
396
397		[addTrack setObject: [track objectForKey: @"title"] forKey: @"title"];
398		[addTrack setObject: [track objectForKey: @"length"] forKey: @"length"];
399		[addTrack setObject: [track objectForKey: @"type"] forKey: @"type"];
400		[addTrack setObject: [NSString stringWithFormat: @"%d", row+1] forKey: @"index"];
401
402		[tracks addObject: [addTrack autorelease]];
403	}
404
405	// add properties for tracks and cd to proplist
406	[cdProperties setObject: tracks forKey: @"tracks"];
407	[propertyList setObject: cdProperties forKey: [toc objectForKey: @"cddbid"]];
408
409	// Set property list of paste board
410	[pboard declareTypes: [NSArray arrayWithObject: @"AudioCDPboardType"] owner: self];
411	[pboard setPropertyList: propertyList forType: @"AudioCDPboardType"];
412	RELEASE(propertyList);
413
414	return YES;
415}
416
417- (id)validRequestorForSendType: (NSString *)sendType
418    		         returnType: (NSString *)returnType
419{
420	if (!returnType && [sendType isEqual: @"AudioCDPboardType"]) {
421		if ([trackListView numberOfSelectedRows] > 0)
422			return self;
423	}
424	return nil;
425}
426
427- (BOOL)writeSelectionToPasteboard: (NSPasteboard *)pboard
428							 types: (NSArray *)types
429{
430	BOOL ret;
431	id row;
432	NSMutableArray *array = [NSMutableArray new];
433	NSEnumerator *selectedRows = [trackListView selectedRowEnumerator];
434
435	if ([types containsObject: @"AudioCDPboardType"] == NO) {
436		return NO;
437	}
438
439	/*
440	 * Add selected rows to the array and make myself write the
441	 * corresponding track data to the pasteboard.
442	 */
443	while ((row = [selectedRows nextObject]) != 0) {
444		[array addObject: row];
445	}
446
447	ret = [self tableView: trackListView writeRows: array toPasteboard: pboard];
448
449	RELEASE(array);
450	return ret;
451}
452
453+ (void)initialize
454{
455	static BOOL initialized = NO;
456
457    /* Make sure code only gets executed once. */
458	if (initialized == YES) return;
459	initialized = YES;
460
461	[NSApp registerServicesMenuSendTypes: [NSArray arrayWithObjects: @"AudioCDPboardType", nil]
462                    returnTypes: nil];
463
464	return;
465}
466
467+ (id) sharedTrackList
468{
469	if (sharedTrackList == nil) {
470		sharedTrackList = [[TrackList alloc] init];
471	}
472
473	return sharedTrackList;
474}
475
476@end
477