xref: /haiku/src/apps/mediaplayer/playlist/Playlist.cpp (revision 73ad2473e7874b3702cf5b0fdf4c81b747812ed9)
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: [%" B_PRIx32 "]\n", strerror(err),
564 						err);
565 				}
566 			} else
567 				printf("Error - No File Found in playlist\n");
568 		}
569 	} else if (_IsBinaryPlaylist(mimeString)) {
570 		BFile file(&ref, B_READ_ONLY);
571 		Playlist temp;
572 		if (temp.Unflatten(&file) == B_OK)
573 			playlist->AdoptPlaylist(temp, playlist->CountItems());
574 	}
575 }
576 
577 
578 /*static*/ void
579 Playlist::AppendQueryToPlaylist(const entry_ref& ref, Playlist* playlist)
580 {
581 	BQueryFile query(&ref);
582 	if (query.InitCheck() != B_OK)
583 		return;
584 
585 	entry_ref foundRef;
586 	while (query.GetNextRef(&foundRef) == B_OK) {
587 		PlaylistItem* item = new (std::nothrow) FilePlaylistItem(foundRef);
588 		if (item == NULL || !playlist->AddItem(item))
589 			delete item;
590 	}
591 }
592 
593 
594 void
595 Playlist::NotifyImportFailed()
596 {
597 	BAutolock _(this);
598 	_NotifyImportFailed();
599 }
600 
601 
602 /*static*/ bool
603 Playlist::ExtraMediaExists(Playlist* playlist, const entry_ref& ref)
604 {
605 	BString exceptExtension = _GetExceptExtension(BPath(&ref).Path());
606 
607 	for (int32 i = 0; i < playlist->CountItems(); i++) {
608 		FilePlaylistItem* compare = dynamic_cast<FilePlaylistItem*>(playlist->ItemAt(i));
609 		if (compare == NULL)
610 			continue;
611 		if (compare->Ref() != ref
612 				&& _GetExceptExtension(BPath(&compare->Ref()).Path()) == exceptExtension )
613 			return true;
614 	}
615 	return false;
616 }
617 
618 
619 // #pragma mark - private
620 
621 
622 /*static*/ bool
623 Playlist::_IsImageFile(const BString& mimeString)
624 {
625 	BMimeType superType;
626 	BMimeType fileType(mimeString.String());
627 
628 	if (fileType.GetSupertype(&superType) != B_OK)
629 		return false;
630 
631 	if (superType == "image")
632 		return true;
633 
634 	return false;
635 }
636 
637 
638 /*static*/ bool
639 Playlist::_IsMediaFile(const BString& mimeString)
640 {
641 	BMimeType superType;
642 	BMimeType fileType(mimeString.String());
643 
644 	if (fileType.GetSupertype(&superType) != B_OK)
645 		return false;
646 
647 	// try a shortcut first
648 	if (superType == "audio" || superType == "video")
649 		return true;
650 
651 	// Look through our supported types
652 	app_info appInfo;
653 	if (be_app->GetAppInfo(&appInfo) != B_OK)
654 		return false;
655 	BFile appFile(&appInfo.ref, B_READ_ONLY);
656 	if (appFile.InitCheck() != B_OK)
657 		return false;
658 	BMessage types;
659 	BAppFileInfo appFileInfo(&appFile);
660 	if (appFileInfo.GetSupportedTypes(&types) != B_OK)
661 		return false;
662 
663 	const char* type;
664 	for (int32 i = 0; types.FindString("types", i, &type) == B_OK; i++) {
665 		if (strcasecmp(mimeString.String(), type) == 0)
666 			return true;
667 	}
668 
669 	return false;
670 }
671 
672 
673 /*static*/ bool
674 Playlist::_IsTextPlaylist(const BString& mimeString)
675 {
676 	return mimeString.Compare(kTextPlaylistMimeString) == 0;
677 }
678 
679 
680 /*static*/ bool
681 Playlist::_IsBinaryPlaylist(const BString& mimeString)
682 {
683 	return mimeString.Compare(kBinaryPlaylistMimeString) == 0;
684 }
685 
686 
687 /*static*/ bool
688 Playlist::_IsPlaylist(const BString& mimeString)
689 {
690 	return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString);
691 }
692 
693 
694 /*static*/ bool
695 Playlist::_IsQuery(const BString& mimeString)
696 {
697 	return mimeString.Compare(BQueryFile::MimeType()) == 0;
698 }
699 
700 
701 /*static*/ BString
702 Playlist::_MIMEString(const entry_ref* ref)
703 {
704 	BFile file(ref, B_READ_ONLY);
705 	BNodeInfo nodeInfo(&file);
706 	char mimeString[B_MIME_TYPE_LENGTH];
707 	if (nodeInfo.GetType(mimeString) != B_OK) {
708 		BMimeType type;
709 		if (BMimeType::GuessMimeType(ref, &type) != B_OK)
710 			return BString();
711 
712 		strlcpy(mimeString, type.Type(), B_MIME_TYPE_LENGTH);
713 		nodeInfo.SetType(type.Type());
714 	}
715 	return BString(mimeString);
716 }
717 
718 
719 // _BindExtraMedia() searches additional videos and audios
720 // and addes them as extra medias.
721 /*static*/ void
722 Playlist::_BindExtraMedia(PlaylistItem* item)
723 {
724 	FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item);
725 	if (!fileItem)
726 		return;
727 
728 	// If the media file is foo.mp3, _BindExtraMedia() searches foo.avi.
729 	BPath mediaFilePath(&fileItem->Ref());
730 	BString mediaFilePathString = mediaFilePath.Path();
731 	BPath dirPath;
732 	mediaFilePath.GetParent(&dirPath);
733 	BDirectory dir(dirPath.Path());
734 	if (dir.InitCheck() != B_OK)
735 		return;
736 
737 	BEntry entry;
738 	BString entryPathString;
739 	while (dir.GetNextEntry(&entry, true) == B_OK) {
740 		if (!entry.IsFile())
741 			continue;
742 		entryPathString = BPath(&entry).Path();
743 		if (entryPathString != mediaFilePathString
744 				&& _GetExceptExtension(entryPathString) == _GetExceptExtension(mediaFilePathString)) {
745 			_BindExtraMedia(fileItem, entry);
746 		}
747 	}
748 }
749 
750 
751 /*static*/ void
752 Playlist::_BindExtraMedia(FilePlaylistItem* fileItem, const BEntry& entry)
753 {
754 	entry_ref ref;
755 	entry.GetRef(&ref);
756 	BString mimeString = _MIMEString(&ref);
757 	if (_IsMediaFile(mimeString)) {
758 		fileItem->AddRef(ref);
759 	} else if (_IsImageFile(mimeString)) {
760 		fileItem->AddImageRef(ref);
761 	}
762 }
763 
764 
765 /*static*/ BString
766 Playlist::_GetExceptExtension(const BString& path)
767 {
768 	int32 periodPos = path.FindLast('.');
769 	if (periodPos <= path.FindLast('/'))
770 		return path;
771 	return BString(path.String(), periodPos);
772 }
773 
774 
775 // #pragma mark - notifications
776 
777 
778 void
779 Playlist::_NotifyItemAdded(PlaylistItem* item, int32 index) const
780 {
781 	BList listeners(fListeners);
782 	int32 count = listeners.CountItems();
783 	for (int32 i = 0; i < count; i++) {
784 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
785 		listener->ItemAdded(item, index);
786 	}
787 }
788 
789 
790 void
791 Playlist::_NotifyItemRemoved(int32 index) const
792 {
793 	BList listeners(fListeners);
794 	int32 count = listeners.CountItems();
795 	for (int32 i = 0; i < count; i++) {
796 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
797 		listener->ItemRemoved(index);
798 	}
799 }
800 
801 
802 void
803 Playlist::_NotifyItemsSorted() const
804 {
805 	BList listeners(fListeners);
806 	int32 count = listeners.CountItems();
807 	for (int32 i = 0; i < count; i++) {
808 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
809 		listener->ItemsSorted();
810 	}
811 }
812 
813 
814 void
815 Playlist::_NotifyCurrentItemChanged(int32 newIndex, bool play) const
816 {
817 	BList listeners(fListeners);
818 	int32 count = listeners.CountItems();
819 	for (int32 i = 0; i < count; i++) {
820 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
821 		listener->CurrentItemChanged(newIndex, play);
822 	}
823 }
824 
825 
826 void
827 Playlist::_NotifyImportFailed() const
828 {
829 	BList listeners(fListeners);
830 	int32 count = listeners.CountItems();
831 	for (int32 i = 0; i < count; i++) {
832 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
833 		listener->ImportFailed();
834 	}
835 }
836