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 if ( !ExtraMediaExists(this, ref) ) { 461 AppendToPlaylistRecursive(ref, &subPlaylist); 462 } 463 } 464 465 // At least sort this subsection of the playlist 466 // if the whole playlist is not sorted anymore. 467 if (!sortPlaylist) 468 subPlaylist.Sort(); 469 } 470 471 if (!subPlaylist.IsEmpty()) { 472 // Add to recent documents 473 be_roster->AddToRecentDocuments(&ref, kAppSig); 474 } 475 476 int32 subPlaylistCount = subPlaylist.CountItems(); 477 AdoptPlaylist(subPlaylist, subAppendIndex); 478 subAppendIndex += subPlaylistCount; 479 } 480 if (sortPlaylist) 481 playlist->Sort(); 482 483 if (add) 484 AdoptPlaylist(temporaryPlaylist, appendIndex); 485 486 if (startPlaying) { 487 // open first file 488 SetCurrentItemIndex(0); 489 } 490 } 491 492 493 /*static*/ void 494 Playlist::AppendToPlaylistRecursive(const entry_ref& ref, Playlist* playlist) 495 { 496 // recursively append the ref (dive into folders) 497 BEntry entry(&ref, true); 498 if (entry.InitCheck() != B_OK || !entry.Exists()) 499 return; 500 501 if (entry.IsDirectory()) { 502 BDirectory dir(&entry); 503 if (dir.InitCheck() != B_OK) 504 return; 505 506 entry.Unset(); 507 508 entry_ref subRef; 509 while (dir.GetNextRef(&subRef) == B_OK) { 510 AppendToPlaylistRecursive(subRef, playlist); 511 } 512 } else if (entry.IsFile()) { 513 BString mimeString = _MIMEString(&ref); 514 if (_IsMediaFile(mimeString)) { 515 PlaylistItem* item = new (std::nothrow) FilePlaylistItem(ref); 516 if (!ExtraMediaExists(playlist, ref)) { 517 _BindExtraMedia(item); 518 if (item != NULL && !playlist->AddItem(item)) 519 delete item; 520 } else 521 delete item; 522 } else 523 printf("MIME Type = %s\n", mimeString.String()); 524 } 525 } 526 527 528 /*static*/ void 529 Playlist::AppendPlaylistToPlaylist(const entry_ref& ref, Playlist* playlist) 530 { 531 BEntry entry(&ref, true); 532 if (entry.InitCheck() != B_OK || !entry.Exists()) 533 return; 534 535 BString mimeString = _MIMEString(&ref); 536 if (_IsTextPlaylist(mimeString)) { 537 //printf("RunPlaylist thing\n"); 538 BFile file(&ref, B_READ_ONLY); 539 FileReadWrite lineReader(&file); 540 541 BString str; 542 entry_ref refPath; 543 status_t err; 544 BPath path; 545 while (lineReader.Next(str)) { 546 str = str.RemoveFirst("file://"); 547 str = str.RemoveLast(".."); 548 path = BPath(str.String()); 549 printf("Line %s\n", path.Path()); 550 if (path.Path() != NULL) { 551 if ((err = get_ref_for_path(path.Path(), &refPath)) == B_OK) { 552 PlaylistItem* item 553 = new (std::nothrow) FilePlaylistItem(refPath); 554 if (item == NULL || !playlist->AddItem(item)) 555 delete item; 556 } else 557 printf("Error - %s: [%lx]\n", strerror(err), (int32) err); 558 } else 559 printf("Error - No File Found in playlist\n"); 560 } 561 } else if (_IsBinaryPlaylist(mimeString)) { 562 BFile file(&ref, B_READ_ONLY); 563 Playlist temp; 564 if (temp.Unflatten(&file) == B_OK) 565 playlist->AdoptPlaylist(temp, playlist->CountItems()); 566 } 567 } 568 569 570 /*static*/ void 571 Playlist::AppendQueryToPlaylist(const entry_ref& ref, Playlist* playlist) 572 { 573 BQueryFile query(&ref); 574 if (query.InitCheck() != B_OK) 575 return; 576 577 entry_ref foundRef; 578 while (query.GetNextRef(&foundRef) == B_OK) { 579 PlaylistItem* item = new (std::nothrow) FilePlaylistItem(foundRef); 580 if (item == NULL || !playlist->AddItem(item)) 581 delete item; 582 } 583 } 584 585 586 void 587 Playlist::NotifyImportFailed() 588 { 589 BAutolock _(this); 590 _NotifyImportFailed(); 591 } 592 593 594 /*static*/ bool 595 Playlist::ExtraMediaExists(Playlist* playlist, const entry_ref& ref) 596 { 597 BString exceptExtension = _GetExceptExtension(BPath(&ref).Path()); 598 599 for (int32 i = 0; i < playlist->CountItems(); i++) { 600 FilePlaylistItem* compare = dynamic_cast<FilePlaylistItem*>(playlist->ItemAt(i)); 601 if (compare == NULL) 602 continue; 603 if (compare->Ref() != ref 604 && _GetExceptExtension(BPath(&compare->Ref()).Path()) == exceptExtension ) 605 return true; 606 } 607 return false; 608 } 609 610 611 // #pragma mark - private 612 613 614 /*static*/ bool 615 Playlist::_IsImageFile(const BString& mimeString) 616 { 617 BMimeType superType; 618 BMimeType fileType(mimeString.String()); 619 620 if (fileType.GetSupertype(&superType) != B_OK) 621 return false; 622 623 if (superType == "image") 624 return true; 625 626 return false; 627 } 628 629 630 /*static*/ bool 631 Playlist::_IsMediaFile(const BString& mimeString) 632 { 633 BMimeType superType; 634 BMimeType fileType(mimeString.String()); 635 636 if (fileType.GetSupertype(&superType) != B_OK) 637 return false; 638 639 // try a shortcut first 640 if (superType == "audio" || superType == "video") 641 return true; 642 643 // Look through our supported types 644 app_info appInfo; 645 if (be_app->GetAppInfo(&appInfo) != B_OK) 646 return false; 647 BFile appFile(&appInfo.ref, B_READ_ONLY); 648 if (appFile.InitCheck() != B_OK) 649 return false; 650 BMessage types; 651 BAppFileInfo appFileInfo(&appFile); 652 if (appFileInfo.GetSupportedTypes(&types) != B_OK) 653 return false; 654 655 const char* type; 656 for (int32 i = 0; types.FindString("types", i, &type) == B_OK; i++) { 657 if (strcasecmp(mimeString.String(), type) == 0) 658 return true; 659 } 660 661 return false; 662 } 663 664 665 /*static*/ bool 666 Playlist::_IsTextPlaylist(const BString& mimeString) 667 { 668 return mimeString.Compare(kTextPlaylistMimeString) == 0; 669 } 670 671 672 /*static*/ bool 673 Playlist::_IsBinaryPlaylist(const BString& mimeString) 674 { 675 return mimeString.Compare(kBinaryPlaylistMimeString) == 0; 676 } 677 678 679 /*static*/ bool 680 Playlist::_IsPlaylist(const BString& mimeString) 681 { 682 return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString); 683 } 684 685 686 /*static*/ bool 687 Playlist::_IsQuery(const BString& mimeString) 688 { 689 return mimeString.Compare(BQueryFile::MimeType()) == 0; 690 } 691 692 693 /*static*/ BString 694 Playlist::_MIMEString(const entry_ref* ref) 695 { 696 BFile file(ref, B_READ_ONLY); 697 BNodeInfo nodeInfo(&file); 698 char mimeString[B_MIME_TYPE_LENGTH]; 699 if (nodeInfo.GetType(mimeString) != B_OK) { 700 BMimeType type; 701 if (BMimeType::GuessMimeType(ref, &type) != B_OK) 702 return BString(); 703 704 strlcpy(mimeString, type.Type(), B_MIME_TYPE_LENGTH); 705 nodeInfo.SetType(type.Type()); 706 } 707 return BString(mimeString); 708 } 709 710 711 // _BindExtraMedia() searches additional videos and audios 712 // and addes them as extra medias. 713 /*static*/ void 714 Playlist::_BindExtraMedia(PlaylistItem* item) 715 { 716 FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item); 717 if (!item) 718 return; 719 720 // If the media file is foo.mp3, _BindExtraMedia() searches foo.avi. 721 BPath mediaFilePath(&fileItem->Ref()); 722 BString mediaFilePathString = mediaFilePath.Path(); 723 BPath dirPath; 724 mediaFilePath.GetParent(&dirPath); 725 BDirectory dir(dirPath.Path()); 726 if (dir.InitCheck() != B_OK) 727 return; 728 729 BEntry entry; 730 BString entryPathString; 731 while (dir.GetNextEntry(&entry, true) == B_OK) { 732 if (!entry.IsFile()) 733 continue; 734 entryPathString = BPath(&entry).Path(); 735 if (entryPathString != mediaFilePathString 736 && _GetExceptExtension(entryPathString) == _GetExceptExtension(mediaFilePathString)) { 737 _BindExtraMedia(fileItem, entry); 738 } 739 } 740 } 741 742 743 /*static*/ void 744 Playlist::_BindExtraMedia(FilePlaylistItem* fileItem, const BEntry& entry) 745 { 746 entry_ref ref; 747 entry.GetRef(&ref); 748 BString mimeString = _MIMEString(&ref); 749 if (_IsMediaFile(mimeString)) { 750 fileItem->AddRef(ref); 751 } else if (_IsImageFile(mimeString)) { 752 fileItem->AddImageRef(ref); 753 } 754 } 755 756 757 /*static*/ BString 758 Playlist::_GetExceptExtension(const BString& path) 759 { 760 int32 periodPos = path.FindLast('.'); 761 if (periodPos <= path.FindLast('/')) 762 return path; 763 return BString(path.String(), periodPos); 764 } 765 766 767 // #pragma mark - notifications 768 769 770 void 771 Playlist::_NotifyItemAdded(PlaylistItem* item, int32 index) const 772 { 773 BList listeners(fListeners); 774 int32 count = listeners.CountItems(); 775 for (int32 i = 0; i < count; i++) { 776 Listener* listener = (Listener*)listeners.ItemAtFast(i); 777 listener->ItemAdded(item, index); 778 } 779 } 780 781 782 void 783 Playlist::_NotifyItemRemoved(int32 index) const 784 { 785 BList listeners(fListeners); 786 int32 count = listeners.CountItems(); 787 for (int32 i = 0; i < count; i++) { 788 Listener* listener = (Listener*)listeners.ItemAtFast(i); 789 listener->ItemRemoved(index); 790 } 791 } 792 793 794 void 795 Playlist::_NotifyItemsSorted() const 796 { 797 BList listeners(fListeners); 798 int32 count = listeners.CountItems(); 799 for (int32 i = 0; i < count; i++) { 800 Listener* listener = (Listener*)listeners.ItemAtFast(i); 801 listener->ItemsSorted(); 802 } 803 } 804 805 806 void 807 Playlist::_NotifyCurrentItemChanged(int32 newIndex) const 808 { 809 BList listeners(fListeners); 810 int32 count = listeners.CountItems(); 811 for (int32 i = 0; i < count; i++) { 812 Listener* listener = (Listener*)listeners.ItemAtFast(i); 813 listener->CurrentItemChanged(newIndex); 814 } 815 } 816 817 818 void 819 Playlist::_NotifyImportFailed() const 820 { 821 BList listeners(fListeners); 822 int32 count = listeners.CountItems(); 823 for (int32 i = 0; i < count; i++) { 824 Listener* listener = (Listener*)listeners.ItemAtFast(i); 825 listener->ImportFailed(); 826 } 827 } 828