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