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