1/* 2 * Playlist.cpp - Media Player for the Haiku Operating System 3 * 4 * Copyright (C) 2006 Marcus Overhagen <marcus@overhagen.de> 5 * Copyright (C) 2007-2009 Stephan A��mus <superstippi@gmx.de> (MIT ok) 6 * Copyright (C) 2008-2009 Fredrik Mod��en <[FirstName]@[LastName].se> (MIT ok) 7 * 8 * Released under the terms of the MIT license. 9 */ 10 11 12#include "Playlist.h" 13 14#include <debugger.h> 15#include <new> 16#include <stdio.h> 17#include <strings.h> 18 19#include <AppFileInfo.h> 20#include <Application.h> 21#include <Autolock.h> 22#include <Directory.h> 23#include <Entry.h> 24#include <File.h> 25#include <Message.h> 26#include <Mime.h> 27#include <NodeInfo.h> 28#include <Path.h> 29#include <Roster.h> 30#include <String.h> 31 32#include <QueryFile.h> 33 34#include "FilePlaylistItem.h" 35#include "FileReadWrite.h" 36#include "MainApp.h" 37 38using std::nothrow; 39 40// TODO: using BList for objects is bad, replace it with a template 41 42Playlist::Listener::Listener() {} 43Playlist::Listener::~Listener() {} 44void Playlist::Listener::ItemAdded(PlaylistItem* item, int32 index) {} 45void Playlist::Listener::ItemRemoved(int32 index) {} 46void Playlist::Listener::ItemsSorted() {} 47void Playlist::Listener::CurrentItemChanged(int32 newIndex, bool play) {} 48void Playlist::Listener::ImportFailed() {} 49 50 51// #pragma mark - 52 53 54static void 55make_item_compare_string(const PlaylistItem* item, char* buffer, 56 size_t bufferSize) 57{ 58 // TODO: Maybe "location" would be useful here as well. 59// snprintf(buffer, bufferSize, "%s - %s - %0*ld - %s", 60// item->Author().String(), 61// item->Album().String(), 62// 3, item->TrackNumber(), 63// item->Title().String()); 64 snprintf(buffer, bufferSize, "%s", item->LocationURI().String()); 65} 66 67 68static int 69playlist_item_compare(const void* _item1, const void* _item2) 70{ 71 // compare complete path 72 const PlaylistItem* item1 = *(const PlaylistItem**)_item1; 73 const PlaylistItem* item2 = *(const PlaylistItem**)_item2; 74 75 static const size_t bufferSize = 1024; 76 char string1[bufferSize]; 77 make_item_compare_string(item1, string1, bufferSize); 78 char string2[bufferSize]; 79 make_item_compare_string(item2, string2, bufferSize); 80 81 return strcmp(string1, string2); 82} 83 84 85// #pragma mark - 86 87 88Playlist::Playlist() 89 : 90 BLocker("playlist lock"), 91 fItems(), 92 fCurrentIndex(-1) 93{ 94} 95 96 97Playlist::~Playlist() 98{ 99 MakeEmpty(); 100 101 if (fListeners.CountItems() > 0) 102 debugger("Playlist::~Playlist() - there are still listeners attached!"); 103} 104 105 106// #pragma mark - archiving 107 108 109static const char* kItemArchiveKey = "item"; 110 111 112status_t 113Playlist::Unarchive(const BMessage* archive) 114{ 115 if (archive == NULL) 116 return B_BAD_VALUE; 117 118 MakeEmpty(); 119 120 BMessage itemArchive; 121 for (int32 i = 0; 122 archive->FindMessage(kItemArchiveKey, i, &itemArchive) == B_OK; i++) { 123 124 BArchivable* archivable = instantiate_object(&itemArchive); 125 PlaylistItem* item = dynamic_cast<PlaylistItem*>(archivable); 126 if (!item) { 127 delete archivable; 128 continue; 129 } 130 131 if (!AddItem(item)) { 132 delete item; 133 return B_NO_MEMORY; 134 } 135 } 136 137 return B_OK; 138} 139 140 141status_t 142Playlist::Archive(BMessage* into) const 143{ 144 if (into == NULL) 145 return B_BAD_VALUE; 146 147 int32 count = CountItems(); 148 for (int32 i = 0; i < count; i++) { 149 const PlaylistItem* item = ItemAtFast(i); 150 BMessage itemArchive; 151 status_t ret = item->Archive(&itemArchive); 152 if (ret != B_OK) 153 return ret; 154 ret = into->AddMessage(kItemArchiveKey, &itemArchive); 155 if (ret != B_OK) 156 return ret; 157 } 158 159 return B_OK; 160} 161 162 163const uint32 kPlaylistMagicBytes = 'MPPL'; 164const char* kTextPlaylistMimeString = "text/x-playlist"; 165const char* kBinaryPlaylistMimeString = "application/x-vnd.haiku-playlist"; 166 167status_t 168Playlist::Unflatten(BDataIO* stream) 169{ 170 if (stream == NULL) 171 return B_BAD_VALUE; 172 173 uint32 magicBytes; 174 ssize_t read = stream->Read(&magicBytes, 4); 175 if (read != 4) { 176 if (read < 0) 177 return (status_t)read; 178 return B_IO_ERROR; 179 } 180 181 if (B_LENDIAN_TO_HOST_INT32(magicBytes) != kPlaylistMagicBytes) 182 return B_BAD_VALUE; 183 184 BMessage archive; 185 status_t ret = archive.Unflatten(stream); 186 if (ret != B_OK) 187 return ret; 188 189 return Unarchive(&archive); 190} 191 192 193status_t 194Playlist::Flatten(BDataIO* stream) const 195{ 196 if (stream == NULL) 197 return B_BAD_VALUE; 198 199 BMessage archive; 200 status_t ret = Archive(&archive); 201 if (ret != B_OK) 202 return ret; 203 204 uint32 magicBytes = B_HOST_TO_LENDIAN_INT32(kPlaylistMagicBytes); 205 ssize_t written = stream->Write(&magicBytes, 4); 206 if (written != 4) { 207 if (written < 0) 208 return (status_t)written; 209 return B_IO_ERROR; 210 } 211 212 return archive.Flatten(stream); 213} 214 215 216// #pragma mark - list access 217 218 219void 220Playlist::MakeEmpty(bool deleteItems) 221{ 222 int32 count = CountItems(); 223 for (int32 i = count - 1; i >= 0; i--) { 224 PlaylistItem* item = RemoveItem(i, false); 225 _NotifyItemRemoved(i); 226 if (deleteItems) 227 item->ReleaseReference(); 228 } 229 SetCurrentItemIndex(-1); 230} 231 232 233int32 234Playlist::CountItems() const 235{ 236 return fItems.CountItems(); 237} 238 239 240bool 241Playlist::IsEmpty() const 242{ 243 return fItems.IsEmpty(); 244} 245 246 247void 248Playlist::Sort() 249{ 250 fItems.SortItems(playlist_item_compare); 251 _NotifyItemsSorted(); 252} 253 254 255bool 256Playlist::AddItem(PlaylistItem* item) 257{ 258 return AddItem(item, CountItems()); 259} 260 261 262bool 263Playlist::AddItem(PlaylistItem* item, int32 index) 264{ 265 if (!fItems.AddItem(item, index)) 266 return false; 267 268 if (index <= fCurrentIndex) 269 SetCurrentItemIndex(fCurrentIndex + 1, false); 270 271 _NotifyItemAdded(item, index); 272 273 return true; 274} 275 276 277bool 278Playlist::AdoptPlaylist(Playlist& other) 279{ 280 return AdoptPlaylist(other, CountItems()); 281} 282 283 284bool 285Playlist::AdoptPlaylist(Playlist& other, int32 index) 286{ 287 if (&other == this) 288 return false; 289 // NOTE: this is not intended to merge two "equal" playlists 290 // the given playlist is assumed to be a temporary "dummy" 291 if (fItems.AddList(&other.fItems, index)) { 292 // take care of the notifications 293 int32 count = other.CountItems(); 294 for (int32 i = index; i < index + count; i++) { 295 PlaylistItem* item = ItemAtFast(i); 296 _NotifyItemAdded(item, i); 297 } 298 if (index <= fCurrentIndex) 299 SetCurrentItemIndex(fCurrentIndex + count); 300 // empty the other list, so that the PlaylistItems are now ours 301 other.fItems.MakeEmpty(); 302 return true; 303 } 304 return false; 305} 306 307 308PlaylistItem* 309Playlist::RemoveItem(int32 index, bool careAboutCurrentIndex) 310{ 311 PlaylistItem* item = (PlaylistItem*)fItems.RemoveItem(index); 312 if (!item) 313 return NULL; 314 _NotifyItemRemoved(index); 315 316 if (careAboutCurrentIndex) { 317 // fCurrentIndex isn't in sync yet, so might be one too large (if the 318 // removed item was above the currently playing item). 319 if (index < fCurrentIndex) 320 SetCurrentItemIndex(fCurrentIndex - 1, false); 321 else if (index == fCurrentIndex) { 322 if (fCurrentIndex == CountItems()) 323 fCurrentIndex--; 324 SetCurrentItemIndex(fCurrentIndex, true); 325 } 326 } 327 328 return item; 329} 330 331 332int32 333Playlist::IndexOf(PlaylistItem* item) const 334{ 335 return fItems.IndexOf(item); 336} 337 338 339PlaylistItem* 340Playlist::ItemAt(int32 index) const 341{ 342 return (PlaylistItem*)fItems.ItemAt(index); 343} 344 345 346PlaylistItem* 347Playlist::ItemAtFast(int32 index) const 348{ 349 return (PlaylistItem*)fItems.ItemAtFast(index); 350} 351 352 353// #pragma mark - navigation 354 355 356bool 357Playlist::SetCurrentItemIndex(int32 index, bool notify) 358{ 359 bool result = true; 360 if (index >= CountItems()) { 361 index = CountItems() - 1; 362 result = false; 363 notify = false; 364 } 365 if (index < 0) { 366 index = -1; 367 result = false; 368 } 369 if (index == fCurrentIndex && !notify) 370 return result; 371 372 fCurrentIndex = index; 373 _NotifyCurrentItemChanged(fCurrentIndex, notify); 374 return result; 375} 376 377 378int32 379Playlist::CurrentItemIndex() const 380{ 381 return fCurrentIndex; 382} 383 384 385void 386Playlist::GetSkipInfo(bool* canSkipPrevious, bool* canSkipNext) const 387{ 388 if (canSkipPrevious) 389 *canSkipPrevious = fCurrentIndex > 0; 390 if (canSkipNext) 391 *canSkipNext = fCurrentIndex < CountItems() - 1; 392} 393 394 395// pragma mark - 396 397 398bool 399Playlist::AddListener(Listener* listener) 400{ 401 BAutolock _(this); 402 if (listener && !fListeners.HasItem(listener)) 403 return fListeners.AddItem(listener); 404 return false; 405} 406 407 408void 409Playlist::RemoveListener(Listener* listener) 410{ 411 BAutolock _(this); 412 fListeners.RemoveItem(listener); 413} 414 415 416// #pragma mark - support 417 418 419void 420Playlist::AppendItems(const BMessage* refsReceivedMessage, int32 appendIndex, 421 bool sortItems) 422{ 423 // the playlist is replaced by the refs in the message 424 // or the refs are appended at the appendIndex 425 // in the existing playlist 426 if (appendIndex == APPEND_INDEX_APPEND_LAST) 427 appendIndex = CountItems(); 428 429 bool add = appendIndex != APPEND_INDEX_REPLACE_PLAYLIST; 430 431 if (!add) 432 MakeEmpty(); 433 434 bool startPlaying = CountItems() == 0; 435 436 Playlist temporaryPlaylist; 437 Playlist* playlist = add ? &temporaryPlaylist : this; 438 bool hasSavedPlaylist = false; 439 440 // TODO: This is not very fair, we should abstract from 441 // entry ref representation and support more URLs. 442 BMessage archivedUrl; 443 if (refsReceivedMessage->FindMessage("mediaplayer:url", &archivedUrl) 444 == B_OK) { 445 BUrl url(&archivedUrl); 446 AddItem(new UrlPlaylistItem(url)); 447 } 448 449 entry_ref ref; 450 int32 subAppendIndex = CountItems(); 451 for (int i = 0; refsReceivedMessage->FindRef("refs", i, &ref) == B_OK; 452 i++) { 453 Playlist subPlaylist; 454 BString type = _MIMEString(&ref); 455 456 if (_IsPlaylist(type)) { 457 AppendPlaylistToPlaylist(ref, &subPlaylist); 458 // Do not sort the whole playlist anymore, as that 459 // will screw up the ordering in the saved playlist. 460 hasSavedPlaylist = true; 461 } else { 462 if (_IsQuery(type)) 463 AppendQueryToPlaylist(ref, &subPlaylist); 464 else { 465 PlaylistFileReader* reader = PlaylistFileReader::GenerateReader(ref); 466 if (reader != NULL) { 467 reader->AppendToPlaylist(ref, &subPlaylist); 468 delete reader; 469 } else { 470 if (!_ExtraMediaExists(this, ref)) { 471 AppendToPlaylistRecursive(ref, &subPlaylist); 472 } 473 } 474 } 475 476 // At least sort this subsection of the playlist 477 // if the whole playlist is not sorted anymore. 478 if (sortItems && hasSavedPlaylist) 479 subPlaylist.Sort(); 480 } 481 482 if (!subPlaylist.IsEmpty()) { 483 // Add to recent documents 484 be_roster->AddToRecentDocuments(&ref, kAppSig); 485 } 486 487 int32 subPlaylistCount = subPlaylist.CountItems(); 488 AdoptPlaylist(subPlaylist, subAppendIndex); 489 subAppendIndex += subPlaylistCount; 490 } 491 492 if (sortItems) 493 playlist->Sort(); 494 495 if (add) 496 AdoptPlaylist(temporaryPlaylist, appendIndex); 497 498 if (startPlaying) { 499 // open first file 500 SetCurrentItemIndex(0); 501 } 502} 503 504 505/*static*/ void 506Playlist::AppendToPlaylistRecursive(const entry_ref& ref, Playlist* playlist) 507{ 508 // recursively append the ref (dive into folders) 509 BEntry entry(&ref, true); 510 if (entry.InitCheck() != B_OK || !entry.Exists()) 511 return; 512 513 if (entry.IsDirectory()) { 514 BDirectory dir(&entry); 515 if (dir.InitCheck() != B_OK) 516 return; 517 518 entry.Unset(); 519 520 entry_ref subRef; 521 while (dir.GetNextRef(&subRef) == B_OK) { 522 AppendToPlaylistRecursive(subRef, playlist); 523 } 524 } else if (entry.IsFile()) { 525 BString mimeString = _MIMEString(&ref); 526 if (_IsMediaFile(mimeString)) { 527 PlaylistItem* item = new (std::nothrow) FilePlaylistItem(ref); 528 if (!_ExtraMediaExists(playlist, ref)) { 529 _BindExtraMedia(item); 530 if (item != NULL && !playlist->AddItem(item)) 531 delete item; 532 } else 533 delete item; 534 } else 535 printf("MIME Type = %s\n", mimeString.String()); 536 } 537} 538 539 540/*static*/ void 541Playlist::AppendPlaylistToPlaylist(const entry_ref& ref, Playlist* playlist) 542{ 543 BEntry entry(&ref, true); 544 if (entry.InitCheck() != B_OK || !entry.Exists()) 545 return; 546 547 BString mimeString = _MIMEString(&ref); 548 if (_IsTextPlaylist(mimeString)) { 549 //printf("RunPlaylist thing\n"); 550 BFile file(&ref, B_READ_ONLY); 551 FileReadWrite lineReader(&file); 552 553 BString str; 554 entry_ref refPath; 555 status_t err; 556 BPath path; 557 while (lineReader.Next(str)) { 558 str = str.RemoveFirst("file://"); 559 str = str.RemoveLast(".."); 560 path = BPath(str.String()); 561 printf("Line %s\n", path.Path()); 562 if (path.Path() != NULL) { 563 if ((err = get_ref_for_path(path.Path(), &refPath)) == B_OK) { 564 PlaylistItem* item 565 = new (std::nothrow) FilePlaylistItem(refPath); 566 if (item == NULL || !playlist->AddItem(item)) 567 delete item; 568 } else { 569 printf("Error - %s: [%" B_PRIx32 "]\n", strerror(err), 570 err); 571 } 572 } else 573 printf("Error - No File Found in playlist\n"); 574 } 575 } else if (_IsBinaryPlaylist(mimeString)) { 576 BFile file(&ref, B_READ_ONLY); 577 Playlist temp; 578 if (temp.Unflatten(&file) == B_OK) 579 playlist->AdoptPlaylist(temp, playlist->CountItems()); 580 } 581} 582 583 584/*static*/ void 585Playlist::AppendQueryToPlaylist(const entry_ref& ref, Playlist* playlist) 586{ 587 BQueryFile query(&ref); 588 if (query.InitCheck() != B_OK) 589 return; 590 591 entry_ref foundRef; 592 while (query.GetNextRef(&foundRef) == B_OK) { 593 PlaylistItem* item = new (std::nothrow) FilePlaylistItem(foundRef); 594 if (item == NULL || !playlist->AddItem(item)) 595 delete item; 596 } 597} 598 599 600void 601Playlist::NotifyImportFailed() 602{ 603 BAutolock _(this); 604 _NotifyImportFailed(); 605} 606 607 608/*static*/ bool 609Playlist::ExtraMediaExists(Playlist* playlist, PlaylistItem* item) 610{ 611 FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item); 612 if (fileItem != NULL) 613 return _ExtraMediaExists(playlist, fileItem->Ref()); 614 615 // If we are here let's see if it is an url 616 UrlPlaylistItem* urlItem = dynamic_cast<UrlPlaylistItem*>(item); 617 if (urlItem == NULL) 618 return true; 619 620 return _ExtraMediaExists(playlist, urlItem->Url()); 621} 622 623 624// #pragma mark - private 625 626 627/*static*/ bool 628Playlist::_ExtraMediaExists(Playlist* playlist, const entry_ref& ref) 629{ 630 BString exceptExtension = _GetExceptExtension(BPath(&ref).Path()); 631 632 for (int32 i = 0; i < playlist->CountItems(); i++) { 633 FilePlaylistItem* compare = dynamic_cast<FilePlaylistItem*>(playlist->ItemAt(i)); 634 if (compare == NULL) 635 continue; 636 if (compare->Ref() != ref 637 && _GetExceptExtension(BPath(&compare->Ref()).Path()) == exceptExtension ) 638 return true; 639 } 640 return false; 641} 642 643 644/*static*/ bool 645Playlist::_ExtraMediaExists(Playlist* playlist, BUrl url) 646{ 647 for (int32 i = 0; i < playlist->CountItems(); i++) { 648 UrlPlaylistItem* compare 649 = dynamic_cast<UrlPlaylistItem*>(playlist->ItemAt(i)); 650 if (compare == NULL) 651 continue; 652 if (compare->Url() == url) 653 return true; 654 } 655 return false; 656} 657 658 659/*static*/ bool 660Playlist::_IsImageFile(const BString& mimeString) 661{ 662 BMimeType superType; 663 BMimeType fileType(mimeString.String()); 664 665 if (fileType.GetSupertype(&superType) != B_OK) 666 return false; 667 668 if (superType == "image") 669 return true; 670 671 return false; 672} 673 674 675/*static*/ bool 676Playlist::_IsMediaFile(const BString& mimeString) 677{ 678 BMimeType superType; 679 BMimeType fileType(mimeString.String()); 680 681 if (fileType.GetSupertype(&superType) != B_OK) 682 return false; 683 684 // try a shortcut first 685 if (superType == "audio" || superType == "video") 686 return true; 687 688 // Look through our supported types 689 app_info appInfo; 690 if (be_app->GetAppInfo(&appInfo) != B_OK) 691 return false; 692 BFile appFile(&appInfo.ref, B_READ_ONLY); 693 if (appFile.InitCheck() != B_OK) 694 return false; 695 BMessage types; 696 BAppFileInfo appFileInfo(&appFile); 697 if (appFileInfo.GetSupportedTypes(&types) != B_OK) 698 return false; 699 700 const char* type; 701 for (int32 i = 0; types.FindString("types", i, &type) == B_OK; i++) { 702 if (strcasecmp(mimeString.String(), type) == 0) 703 return true; 704 } 705 706 return false; 707} 708 709 710/*static*/ bool 711Playlist::_IsTextPlaylist(const BString& mimeString) 712{ 713 return mimeString.Compare(kTextPlaylistMimeString) == 0; 714} 715 716 717/*static*/ bool 718Playlist::_IsBinaryPlaylist(const BString& mimeString) 719{ 720 return mimeString.Compare(kBinaryPlaylistMimeString) == 0; 721} 722 723 724/*static*/ bool 725Playlist::_IsPlaylist(const BString& mimeString) 726{ 727 return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString); 728} 729 730 731/*static*/ bool 732Playlist::_IsQuery(const BString& mimeString) 733{ 734 return mimeString.Compare(BQueryFile::MimeType()) == 0; 735} 736 737 738/*static*/ BString 739Playlist::_MIMEString(const entry_ref* ref) 740{ 741 BFile file(ref, B_READ_ONLY); 742 BNodeInfo nodeInfo(&file); 743 char mimeString[B_MIME_TYPE_LENGTH]; 744 if (nodeInfo.GetType(mimeString) != B_OK) { 745 BMimeType type; 746 if (BMimeType::GuessMimeType(ref, &type) != B_OK) 747 return BString(); 748 749 strlcpy(mimeString, type.Type(), B_MIME_TYPE_LENGTH); 750 nodeInfo.SetType(type.Type()); 751 } 752 return BString(mimeString); 753} 754 755 756// _BindExtraMedia() searches additional videos and audios 757// and addes them as extra medias. 758/*static*/ void 759Playlist::_BindExtraMedia(PlaylistItem* item) 760{ 761 FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item); 762 if (!fileItem) 763 return; 764 765 // If the media file is foo.mp3, _BindExtraMedia() searches foo.avi. 766 BPath mediaFilePath(&fileItem->Ref()); 767 BString mediaFilePathString = mediaFilePath.Path(); 768 BPath dirPath; 769 mediaFilePath.GetParent(&dirPath); 770 BDirectory dir(dirPath.Path()); 771 if (dir.InitCheck() != B_OK) 772 return; 773 774 BEntry entry; 775 BString entryPathString; 776 while (dir.GetNextEntry(&entry, true) == B_OK) { 777 if (!entry.IsFile()) 778 continue; 779 entryPathString = BPath(&entry).Path(); 780 if (entryPathString != mediaFilePathString 781 && _GetExceptExtension(entryPathString) == _GetExceptExtension(mediaFilePathString)) { 782 _BindExtraMedia(fileItem, entry); 783 } 784 } 785} 786 787 788/*static*/ void 789Playlist::_BindExtraMedia(FilePlaylistItem* fileItem, const BEntry& entry) 790{ 791 entry_ref ref; 792 entry.GetRef(&ref); 793 BString mimeString = _MIMEString(&ref); 794 if (_IsMediaFile(mimeString)) { 795 fileItem->AddRef(ref); 796 } else if (_IsImageFile(mimeString)) { 797 fileItem->AddImageRef(ref); 798 } 799} 800 801 802/*static*/ BString 803Playlist::_GetExceptExtension(const BString& path) 804{ 805 int32 periodPos = path.FindLast('.'); 806 if (periodPos <= path.FindLast('/')) 807 return path; 808 return BString(path.String(), periodPos); 809} 810 811 812// #pragma mark - notifications 813 814 815void 816Playlist::_NotifyItemAdded(PlaylistItem* item, int32 index) const 817{ 818 BList listeners(fListeners); 819 int32 count = listeners.CountItems(); 820 for (int32 i = 0; i < count; i++) { 821 Listener* listener = (Listener*)listeners.ItemAtFast(i); 822 listener->ItemAdded(item, index); 823 } 824} 825 826 827void 828Playlist::_NotifyItemRemoved(int32 index) const 829{ 830 BList listeners(fListeners); 831 int32 count = listeners.CountItems(); 832 for (int32 i = 0; i < count; i++) { 833 Listener* listener = (Listener*)listeners.ItemAtFast(i); 834 listener->ItemRemoved(index); 835 } 836} 837 838 839void 840Playlist::_NotifyItemsSorted() const 841{ 842 BList listeners(fListeners); 843 int32 count = listeners.CountItems(); 844 for (int32 i = 0; i < count; i++) { 845 Listener* listener = (Listener*)listeners.ItemAtFast(i); 846 listener->ItemsSorted(); 847 } 848} 849 850 851void 852Playlist::_NotifyCurrentItemChanged(int32 newIndex, bool play) const 853{ 854 BList listeners(fListeners); 855 int32 count = listeners.CountItems(); 856 for (int32 i = 0; i < count; i++) { 857 Listener* listener = (Listener*)listeners.ItemAtFast(i); 858 listener->CurrentItemChanged(newIndex, play); 859 } 860} 861 862 863void 864Playlist::_NotifyImportFailed() const 865{ 866 BList listeners(fListeners); 867 int32 count = listeners.CountItems(); 868 for (int32 i = 0; i < count; i++) { 869 Listener* listener = (Listener*)listeners.ItemAtFast(i); 870 listener->ImportFailed(); 871 } 872} 873