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
49using std::nothrow;
50
51// TODO: using BList for objects is bad, replace it with a template
52
53Playlist::Listener::Listener() {}
54Playlist::Listener::~Listener() {}
55void Playlist::Listener::ItemAdded(PlaylistItem* item, int32 index) {}
56void Playlist::Listener::ItemRemoved(int32 index) {}
57void Playlist::Listener::ItemsSorted() {}
58void Playlist::Listener::CurrentItemChanged(int32 newIndex, bool play) {}
59void Playlist::Listener::ImportFailed() {}
60
61
62// #pragma mark -
63
64
65static void
66make_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
79static int
80playlist_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
99Playlist::Playlist()
100	:
101	BLocker("playlist lock"),
102	fItems(),
103 	fCurrentIndex(-1)
104{
105}
106
107
108Playlist::~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
120static const char* kItemArchiveKey = "item";
121
122
123status_t
124Playlist::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
152status_t
153Playlist::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
174const uint32 kPlaylistMagicBytes = 'MPPL';
175const char* kTextPlaylistMimeString = "text/x-playlist";
176const char* kBinaryPlaylistMimeString = "application/x-vnd.haiku-playlist";
177
178status_t
179Playlist::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
204status_t
205Playlist::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
230void
231Playlist::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
244int32
245Playlist::CountItems() const
246{
247	return fItems.CountItems();
248}
249
250
251bool
252Playlist::IsEmpty() const
253{
254	return fItems.IsEmpty();
255}
256
257
258void
259Playlist::Sort()
260{
261	fItems.SortItems(playlist_item_compare);
262	_NotifyItemsSorted();
263}
264
265
266bool
267Playlist::AddItem(PlaylistItem* item)
268{
269	return AddItem(item, CountItems());
270}
271
272
273bool
274Playlist::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
288bool
289Playlist::AdoptPlaylist(Playlist& other)
290{
291	return AdoptPlaylist(other, CountItems());
292}
293
294
295bool
296Playlist::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
319PlaylistItem*
320Playlist::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
343int32
344Playlist::IndexOf(PlaylistItem* item) const
345{
346	return fItems.IndexOf(item);
347}
348
349
350PlaylistItem*
351Playlist::ItemAt(int32 index) const
352{
353	return (PlaylistItem*)fItems.ItemAt(index);
354}
355
356
357PlaylistItem*
358Playlist::ItemAtFast(int32 index) const
359{
360	return (PlaylistItem*)fItems.ItemAtFast(index);
361}
362
363
364// #pragma mark - navigation
365
366
367bool
368Playlist::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
389int32
390Playlist::CurrentItemIndex() const
391{
392	return fCurrentIndex;
393}
394
395
396void
397Playlist::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
409bool
410Playlist::AddListener(Listener* listener)
411{
412	BAutolock _(this);
413	if (listener && !fListeners.HasItem(listener))
414		return fListeners.AddItem(listener);
415	return false;
416}
417
418
419void
420Playlist::RemoveListener(Listener* listener)
421{
422	BAutolock _(this);
423	fListeners.RemoveItem(listener);
424}
425
426
427// #pragma mark - support
428
429
430void
431Playlist::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
500Playlist::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
535Playlist::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
577Playlist::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
592void
593Playlist::NotifyImportFailed()
594{
595	BAutolock _(this);
596	_NotifyImportFailed();
597}
598
599
600/*static*/ bool
601Playlist::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
621Playlist::_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
637Playlist::_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
672Playlist::_IsTextPlaylist(const BString& mimeString)
673{
674	return mimeString.Compare(kTextPlaylistMimeString) == 0;
675}
676
677
678/*static*/ bool
679Playlist::_IsBinaryPlaylist(const BString& mimeString)
680{
681	return mimeString.Compare(kBinaryPlaylistMimeString) == 0;
682}
683
684
685/*static*/ bool
686Playlist::_IsPlaylist(const BString& mimeString)
687{
688	return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString);
689}
690
691
692/*static*/ bool
693Playlist::_IsQuery(const BString& mimeString)
694{
695	return mimeString.Compare(BQueryFile::MimeType()) == 0;
696}
697
698
699/*static*/ BString
700Playlist::_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
720Playlist::_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
750Playlist::_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
764Playlist::_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
776void
777Playlist::_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
788void
789Playlist::_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
800void
801Playlist::_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
812void
813Playlist::_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
824void
825Playlist::_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