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