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, bool play) {} 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, false); 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 // 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 343 int32 344 Playlist::IndexOf(PlaylistItem* item) const 345 { 346 return fItems.IndexOf(item); 347 } 348 349 350 PlaylistItem* 351 Playlist::ItemAt(int32 index) const 352 { 353 return (PlaylistItem*)fItems.ItemAt(index); 354 } 355 356 357 PlaylistItem* 358 Playlist::ItemAtFast(int32 index) const 359 { 360 return (PlaylistItem*)fItems.ItemAtFast(index); 361 } 362 363 364 // #pragma mark - navigation 365 366 367 bool 368 Playlist::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 389 int32 390 Playlist::CurrentItemIndex() const 391 { 392 return fCurrentIndex; 393 } 394 395 396 void 397 Playlist::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 409 bool 410 Playlist::AddListener(Listener* listener) 411 { 412 BAutolock _(this); 413 if (listener && !fListeners.HasItem(listener)) 414 return fListeners.AddItem(listener); 415 return false; 416 } 417 418 419 void 420 Playlist::RemoveListener(Listener* listener) 421 { 422 BAutolock _(this); 423 fListeners.RemoveItem(listener); 424 } 425 426 427 // #pragma mark - support 428 429 430 void 431 Playlist::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 500 Playlist::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 535 Playlist::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 577 Playlist::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 592 void 593 Playlist::NotifyImportFailed() 594 { 595 BAutolock _(this); 596 _NotifyImportFailed(); 597 } 598 599 600 /*static*/ bool 601 Playlist::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 621 Playlist::_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 637 Playlist::_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 672 Playlist::_IsTextPlaylist(const BString& mimeString) 673 { 674 return mimeString.Compare(kTextPlaylistMimeString) == 0; 675 } 676 677 678 /*static*/ bool 679 Playlist::_IsBinaryPlaylist(const BString& mimeString) 680 { 681 return mimeString.Compare(kBinaryPlaylistMimeString) == 0; 682 } 683 684 685 /*static*/ bool 686 Playlist::_IsPlaylist(const BString& mimeString) 687 { 688 return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString); 689 } 690 691 692 /*static*/ bool 693 Playlist::_IsQuery(const BString& mimeString) 694 { 695 return mimeString.Compare(BQueryFile::MimeType()) == 0; 696 } 697 698 699 /*static*/ BString 700 Playlist::_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 720 Playlist::_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 750 Playlist::_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 764 Playlist::_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 776 void 777 Playlist::_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 788 void 789 Playlist::_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 800 void 801 Playlist::_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 812 void 813 Playlist::_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 824 void 825 Playlist::_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