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