xref: /haiku/src/apps/mediaplayer/playlist/Playlist.cpp (revision 1c09002cbee8e797a0f8bbfc5678dfadd39ee1a7)
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) {}
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);
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 		if (index == fCurrentIndex && index >= CountItems())
329 			SetCurrentItemIndex(CountItems() - 1);
330 		else if (index < fCurrentIndex)
331 			SetCurrentItemIndex(fCurrentIndex - 1);
332 	}
333 
334 	return item;
335 }
336 
337 
338 int32
339 Playlist::IndexOf(PlaylistItem* item) const
340 {
341 	return fItems.IndexOf(item);
342 }
343 
344 
345 PlaylistItem*
346 Playlist::ItemAt(int32 index) const
347 {
348 	return (PlaylistItem*)fItems.ItemAt(index);
349 }
350 
351 
352 PlaylistItem*
353 Playlist::ItemAtFast(int32 index) const
354 {
355 	return (PlaylistItem*)fItems.ItemAtFast(index);
356 }
357 
358 
359 // #pragma mark - navigation
360 
361 
362 bool
363 Playlist::SetCurrentItemIndex(int32 index)
364 {
365 	bool result = true;
366 	if (index >= CountItems()) {
367 		index = CountItems() - 1;
368 		result = false;
369 	}
370 	if (index < 0) {
371 		index = -1;
372 		result = false;
373 	}
374 	if (index == fCurrentIndex)
375 		return result;
376 
377 	fCurrentIndex = index;
378 	_NotifyCurrentItemChanged(fCurrentIndex);
379 	return result;
380 }
381 
382 
383 int32
384 Playlist::CurrentItemIndex() const
385 {
386 	return fCurrentIndex;
387 }
388 
389 
390 void
391 Playlist::GetSkipInfo(bool* canSkipPrevious, bool* canSkipNext) const
392 {
393 	if (canSkipPrevious)
394 		*canSkipPrevious = fCurrentIndex > 0;
395 	if (canSkipNext)
396 		*canSkipNext = fCurrentIndex < CountItems() - 1;
397 }
398 
399 
400 // pragma mark -
401 
402 
403 bool
404 Playlist::AddListener(Listener* listener)
405 {
406 	BAutolock _(this);
407 	if (listener && !fListeners.HasItem(listener))
408 		return fListeners.AddItem(listener);
409 	return false;
410 }
411 
412 
413 void
414 Playlist::RemoveListener(Listener* listener)
415 {
416 	BAutolock _(this);
417 	fListeners.RemoveItem(listener);
418 }
419 
420 
421 // #pragma mark - support
422 
423 
424 void
425 Playlist::AppendRefs(const BMessage* refsReceivedMessage, int32 appendIndex)
426 {
427 	// the playlist is replaced by the refs in the message
428 	// or the refs are appended at the appendIndex
429 	// in the existing playlist
430 	if (appendIndex == APPEND_INDEX_APPEND_LAST)
431 		appendIndex = CountItems();
432 
433 	bool add = appendIndex != APPEND_INDEX_REPLACE_PLAYLIST;
434 
435 	if (!add)
436 		MakeEmpty();
437 
438 	bool startPlaying = CountItems() == 0;
439 
440 	Playlist temporaryPlaylist;
441 	Playlist* playlist = add ? &temporaryPlaylist : this;
442 	bool sortPlaylist = true;
443 
444 	entry_ref ref;
445 	int32 subAppendIndex = CountItems();
446 	for (int i = 0; refsReceivedMessage->FindRef("refs", i, &ref) == B_OK;
447 			i++) {
448 		Playlist subPlaylist;
449 		BString type = _MIMEString(&ref);
450 
451 		if (_IsPlaylist(type)) {
452 			AppendPlaylistToPlaylist(ref, &subPlaylist);
453 			// Do not sort the whole playlist anymore, as that
454 			// will screw up the ordering in the saved playlist.
455 			sortPlaylist = false;
456 		} else {
457 			if (_IsQuery(type))
458 				AppendQueryToPlaylist(ref, &subPlaylist);
459 			else
460 				AppendToPlaylistRecursive(ref, &subPlaylist);
461 
462 			// At least sort this subsection of the playlist
463 			// if the whole playlist is not sorted anymore.
464 			if (!sortPlaylist)
465 				subPlaylist.Sort();
466 		}
467 
468 		if (!subPlaylist.IsEmpty()) {
469 			// Add to recent documents
470 			be_roster->AddToRecentDocuments(&ref, kAppSig);
471 		}
472 
473 		int32 subPlaylistCount = subPlaylist.CountItems();
474 		AdoptPlaylist(subPlaylist, subAppendIndex);
475 		subAppendIndex += subPlaylistCount;
476 	}
477 	if (sortPlaylist)
478 		playlist->Sort();
479 
480 	if (add)
481 		AdoptPlaylist(temporaryPlaylist, appendIndex);
482 
483 	if (startPlaying) {
484 		// open first file
485 		SetCurrentItemIndex(0);
486 	}
487 }
488 
489 
490 /*static*/ void
491 Playlist::AppendToPlaylistRecursive(const entry_ref& ref, Playlist* playlist)
492 {
493 	// recursively append the ref (dive into folders)
494 	BEntry entry(&ref, true);
495 	if (entry.InitCheck() != B_OK || !entry.Exists())
496 		return;
497 
498 	if (entry.IsDirectory()) {
499 		BDirectory dir(&entry);
500 		if (dir.InitCheck() != B_OK)
501 			return;
502 
503 		entry.Unset();
504 
505 		entry_ref subRef;
506 		while (dir.GetNextRef(&subRef) == B_OK) {
507 			AppendToPlaylistRecursive(subRef, playlist);
508 		}
509 	} else if (entry.IsFile()) {
510 		BString mimeString = _MIMEString(&ref);
511 		if (_IsMediaFile(mimeString)) {
512 			PlaylistItem* item = new (std::nothrow) FilePlaylistItem(ref);
513 			if (item != NULL && !playlist->AddItem(item))
514 				delete item;
515 		} else
516 			printf("MIME Type = %s\n", mimeString.String());
517 	}
518 }
519 
520 
521 /*static*/ void
522 Playlist::AppendPlaylistToPlaylist(const entry_ref& ref, Playlist* playlist)
523 {
524 	BEntry entry(&ref, true);
525 	if (entry.InitCheck() != B_OK || !entry.Exists())
526 		return;
527 
528 	BString mimeString = _MIMEString(&ref);
529 	if (_IsTextPlaylist(mimeString)) {
530 		//printf("RunPlaylist thing\n");
531 		BFile file(&ref, B_READ_ONLY);
532 		FileReadWrite lineReader(&file);
533 
534 		BString str;
535 		entry_ref refPath;
536 		status_t err;
537 		BPath path;
538 		while (lineReader.Next(str)) {
539 			str = str.RemoveFirst("file://");
540 			str = str.RemoveLast("..");
541 			path = BPath(str.String());
542 			printf("Line %s\n", path.Path());
543 			if (path.Path() != NULL) {
544 				if ((err = get_ref_for_path(path.Path(), &refPath)) == B_OK) {
545 					PlaylistItem* item
546 						= new (std::nothrow) FilePlaylistItem(refPath);
547 					if (item == NULL || !playlist->AddItem(item))
548 						delete item;
549 				} else
550 					printf("Error - %s: [%lx]\n", strerror(err), (int32) err);
551 			} else
552 				printf("Error - No File Found in playlist\n");
553 		}
554 	} else if (_IsBinaryPlaylist(mimeString)) {
555 		BFile file(&ref, B_READ_ONLY);
556 		Playlist temp;
557 		if (temp.Unflatten(&file) == B_OK)
558 			playlist->AdoptPlaylist(temp, playlist->CountItems());
559 	}
560 }
561 
562 
563 /*static*/ void
564 Playlist::AppendQueryToPlaylist(const entry_ref& ref, Playlist* playlist)
565 {
566 	BQueryFile query(&ref);
567 	if (query.InitCheck() != B_OK)
568 		return;
569 
570 	entry_ref foundRef;
571 	while (query.GetNextRef(&foundRef) == B_OK) {
572 		PlaylistItem* item = new (std::nothrow) FilePlaylistItem(foundRef);
573 		if (item == NULL || !playlist->AddItem(item))
574 			delete item;
575 	}
576 }
577 
578 
579 void
580 Playlist::NotifyImportFailed()
581 {
582 	BAutolock _(this);
583 	_NotifyImportFailed();
584 }
585 
586 
587 // #pragma mark - private
588 
589 
590 /*static*/ bool
591 Playlist::_IsMediaFile(const BString& mimeString)
592 {
593 	BMimeType superType;
594 	BMimeType fileType(mimeString.String());
595 
596 	if (fileType.GetSupertype(&superType) != B_OK)
597 		return false;
598 
599 	// try a shortcut first
600 	if (superType == "audio" || superType == "video")
601 		return true;
602 
603 	// Look through our supported types
604 	app_info appInfo;
605 	if (be_app->GetAppInfo(&appInfo) != B_OK)
606 		return false;
607 	BFile appFile(&appInfo.ref, B_READ_ONLY);
608 	if (appFile.InitCheck() != B_OK)
609 		return false;
610 	BMessage types;
611 	BAppFileInfo appFileInfo(&appFile);
612 	if (appFileInfo.GetSupportedTypes(&types) != B_OK)
613 		return false;
614 
615 	const char* type;
616 	for (int32 i = 0; types.FindString("types", i, &type) == B_OK; i++) {
617 		if (strcasecmp(mimeString.String(), type) == 0)
618 			return true;
619 	}
620 
621 	return false;
622 }
623 
624 
625 /*static*/ bool
626 Playlist::_IsTextPlaylist(const BString& mimeString)
627 {
628 	return mimeString.Compare(kTextPlaylistMimeString) == 0;
629 }
630 
631 
632 /*static*/ bool
633 Playlist::_IsBinaryPlaylist(const BString& mimeString)
634 {
635 	return mimeString.Compare(kBinaryPlaylistMimeString) == 0;
636 }
637 
638 
639 /*static*/ bool
640 Playlist::_IsPlaylist(const BString& mimeString)
641 {
642 	return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString);
643 }
644 
645 
646 /*static*/ bool
647 Playlist::_IsQuery(const BString& mimeString)
648 {
649 	return mimeString.Compare(BQueryFile::MimeType()) == 0;
650 }
651 
652 
653 /*static*/ BString
654 Playlist::_MIMEString(const entry_ref* ref)
655 {
656 	BFile file(ref, B_READ_ONLY);
657 	BNodeInfo nodeInfo(&file);
658 	char mimeString[B_MIME_TYPE_LENGTH];
659 	if (nodeInfo.GetType(mimeString) != B_OK) {
660 		BMimeType type;
661 		if (BMimeType::GuessMimeType(ref, &type) != B_OK)
662 			return BString();
663 
664 		strlcpy(mimeString, type.Type(), B_MIME_TYPE_LENGTH);
665 		nodeInfo.SetType(type.Type());
666 	}
667 	return BString(mimeString);
668 }
669 
670 
671 // #pragma mark - notifications
672 
673 
674 void
675 Playlist::_NotifyItemAdded(PlaylistItem* item, int32 index) const
676 {
677 	BList listeners(fListeners);
678 	int32 count = listeners.CountItems();
679 	for (int32 i = 0; i < count; i++) {
680 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
681 		listener->ItemAdded(item, index);
682 	}
683 }
684 
685 
686 void
687 Playlist::_NotifyItemRemoved(int32 index) const
688 {
689 	BList listeners(fListeners);
690 	int32 count = listeners.CountItems();
691 	for (int32 i = 0; i < count; i++) {
692 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
693 		listener->ItemRemoved(index);
694 	}
695 }
696 
697 
698 void
699 Playlist::_NotifyItemsSorted() const
700 {
701 	BList listeners(fListeners);
702 	int32 count = listeners.CountItems();
703 	for (int32 i = 0; i < count; i++) {
704 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
705 		listener->ItemsSorted();
706 	}
707 }
708 
709 
710 void
711 Playlist::_NotifyCurrentItemChanged(int32 newIndex) const
712 {
713 	BList listeners(fListeners);
714 	int32 count = listeners.CountItems();
715 	for (int32 i = 0; i < count; i++) {
716 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
717 		listener->CurrentItemChanged(newIndex);
718 	}
719 }
720 
721 
722 void
723 Playlist::_NotifyImportFailed() const
724 {
725 	BList listeners(fListeners);
726 	int32 count = listeners.CountItems();
727 	for (int32 i = 0; i < count; i++) {
728 		Listener* listener = (Listener*)listeners.ItemAtFast(i);
729 		listener->ImportFailed();
730 	}
731 }
732