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