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 #include <strings.h> 30 31 #include <AppFileInfo.h> 32 #include <Application.h> 33 #include <Autolock.h> 34 #include <Directory.h> 35 #include <Entry.h> 36 #include <File.h> 37 #include <Message.h> 38 #include <Mime.h> 39 #include <NodeInfo.h> 40 #include <Path.h> 41 #include <Roster.h> 42 #include <String.h> 43 44 #include <QueryFile.h> 45 46 #include "FilePlaylistItem.h" 47 #include "FileReadWrite.h" 48 #include "MainApp.h" 49 50 using std::nothrow; 51 52 // TODO: using BList for objects is bad, replace it with a template 53 54 Playlist::Listener::Listener() {} 55 Playlist::Listener::~Listener() {} 56 void Playlist::Listener::ItemAdded(PlaylistItem* item, int32 index) {} 57 void Playlist::Listener::ItemRemoved(int32 index) {} 58 void Playlist::Listener::ItemsSorted() {} 59 void Playlist::Listener::CurrentItemChanged(int32 newIndex, bool play) {} 60 void Playlist::Listener::ImportFailed() {} 61 62 63 // #pragma mark - 64 65 66 static void 67 make_item_compare_string(const PlaylistItem* item, char* buffer, 68 size_t bufferSize) 69 { 70 // TODO: Maybe "location" would be useful here as well. 71 // snprintf(buffer, bufferSize, "%s - %s - %0*ld - %s", 72 // item->Author().String(), 73 // item->Album().String(), 74 // 3, item->TrackNumber(), 75 // item->Title().String()); 76 snprintf(buffer, bufferSize, "%s", item->LocationURI().String()); 77 } 78 79 80 static int 81 playlist_item_compare(const void* _item1, const void* _item2) 82 { 83 // compare complete path 84 const PlaylistItem* item1 = *(const PlaylistItem**)_item1; 85 const PlaylistItem* item2 = *(const PlaylistItem**)_item2; 86 87 static const size_t bufferSize = 1024; 88 char string1[bufferSize]; 89 make_item_compare_string(item1, string1, bufferSize); 90 char string2[bufferSize]; 91 make_item_compare_string(item2, string2, bufferSize); 92 93 return strcmp(string1, string2); 94 } 95 96 97 // #pragma mark - 98 99 100 Playlist::Playlist() 101 : 102 BLocker("playlist lock"), 103 fItems(), 104 fCurrentIndex(-1) 105 { 106 } 107 108 109 Playlist::~Playlist() 110 { 111 MakeEmpty(); 112 113 if (fListeners.CountItems() > 0) 114 debugger("Playlist::~Playlist() - there are still listeners attached!"); 115 } 116 117 118 // #pragma mark - archiving 119 120 121 static const char* kItemArchiveKey = "item"; 122 123 124 status_t 125 Playlist::Unarchive(const BMessage* archive) 126 { 127 if (archive == NULL) 128 return B_BAD_VALUE; 129 130 MakeEmpty(); 131 132 BMessage itemArchive; 133 for (int32 i = 0; 134 archive->FindMessage(kItemArchiveKey, i, &itemArchive) == B_OK; i++) { 135 136 BArchivable* archivable = instantiate_object(&itemArchive); 137 PlaylistItem* item = dynamic_cast<PlaylistItem*>(archivable); 138 if (!item) { 139 delete archivable; 140 continue; 141 } 142 143 if (!AddItem(item)) { 144 delete item; 145 return B_NO_MEMORY; 146 } 147 } 148 149 return B_OK; 150 } 151 152 153 status_t 154 Playlist::Archive(BMessage* into) const 155 { 156 if (into == NULL) 157 return B_BAD_VALUE; 158 159 int32 count = CountItems(); 160 for (int32 i = 0; i < count; i++) { 161 const PlaylistItem* item = ItemAtFast(i); 162 BMessage itemArchive; 163 status_t ret = item->Archive(&itemArchive); 164 if (ret != B_OK) 165 return ret; 166 ret = into->AddMessage(kItemArchiveKey, &itemArchive); 167 if (ret != B_OK) 168 return ret; 169 } 170 171 return B_OK; 172 } 173 174 175 const uint32 kPlaylistMagicBytes = 'MPPL'; 176 const char* kTextPlaylistMimeString = "text/x-playlist"; 177 const char* kBinaryPlaylistMimeString = "application/x-vnd.haiku-playlist"; 178 179 status_t 180 Playlist::Unflatten(BDataIO* stream) 181 { 182 if (stream == NULL) 183 return B_BAD_VALUE; 184 185 uint32 magicBytes; 186 ssize_t read = stream->Read(&magicBytes, 4); 187 if (read != 4) { 188 if (read < 0) 189 return (status_t)read; 190 return B_IO_ERROR; 191 } 192 193 if (B_LENDIAN_TO_HOST_INT32(magicBytes) != kPlaylistMagicBytes) 194 return B_BAD_VALUE; 195 196 BMessage archive; 197 status_t ret = archive.Unflatten(stream); 198 if (ret != B_OK) 199 return ret; 200 201 return Unarchive(&archive); 202 } 203 204 205 status_t 206 Playlist::Flatten(BDataIO* stream) const 207 { 208 if (stream == NULL) 209 return B_BAD_VALUE; 210 211 BMessage archive; 212 status_t ret = Archive(&archive); 213 if (ret != B_OK) 214 return ret; 215 216 uint32 magicBytes = B_HOST_TO_LENDIAN_INT32(kPlaylistMagicBytes); 217 ssize_t written = stream->Write(&magicBytes, 4); 218 if (written != 4) { 219 if (written < 0) 220 return (status_t)written; 221 return B_IO_ERROR; 222 } 223 224 return archive.Flatten(stream); 225 } 226 227 228 // #pragma mark - list access 229 230 231 void 232 Playlist::MakeEmpty(bool deleteItems) 233 { 234 int32 count = CountItems(); 235 for (int32 i = count - 1; i >= 0; i--) { 236 PlaylistItem* item = RemoveItem(i, false); 237 _NotifyItemRemoved(i); 238 if (deleteItems) 239 item->ReleaseReference(); 240 } 241 SetCurrentItemIndex(-1); 242 } 243 244 245 int32 246 Playlist::CountItems() const 247 { 248 return fItems.CountItems(); 249 } 250 251 252 bool 253 Playlist::IsEmpty() const 254 { 255 return fItems.IsEmpty(); 256 } 257 258 259 void 260 Playlist::Sort() 261 { 262 fItems.SortItems(playlist_item_compare); 263 _NotifyItemsSorted(); 264 } 265 266 267 bool 268 Playlist::AddItem(PlaylistItem* item) 269 { 270 return AddItem(item, CountItems()); 271 } 272 273 274 bool 275 Playlist::AddItem(PlaylistItem* item, int32 index) 276 { 277 if (!fItems.AddItem(item, index)) 278 return false; 279 280 if (index <= fCurrentIndex) 281 SetCurrentItemIndex(fCurrentIndex + 1, false); 282 283 _NotifyItemAdded(item, index); 284 285 return true; 286 } 287 288 289 bool 290 Playlist::AdoptPlaylist(Playlist& other) 291 { 292 return AdoptPlaylist(other, CountItems()); 293 } 294 295 296 bool 297 Playlist::AdoptPlaylist(Playlist& other, int32 index) 298 { 299 if (&other == this) 300 return false; 301 // NOTE: this is not intended to merge two "equal" playlists 302 // the given playlist is assumed to be a temporary "dummy" 303 if (fItems.AddList(&other.fItems, index)) { 304 // take care of the notifications 305 int32 count = other.CountItems(); 306 for (int32 i = index; i < index + count; i++) { 307 PlaylistItem* item = ItemAtFast(i); 308 _NotifyItemAdded(item, i); 309 } 310 if (index <= fCurrentIndex) 311 SetCurrentItemIndex(fCurrentIndex + count); 312 // empty the other list, so that the PlaylistItems are now ours 313 other.fItems.MakeEmpty(); 314 return true; 315 } 316 return false; 317 } 318 319 320 PlaylistItem* 321 Playlist::RemoveItem(int32 index, bool careAboutCurrentIndex) 322 { 323 PlaylistItem* item = (PlaylistItem*)fItems.RemoveItem(index); 324 if (!item) 325 return NULL; 326 _NotifyItemRemoved(index); 327 328 if (careAboutCurrentIndex) { 329 // fCurrentIndex isn't in sync yet, so might be one too large (if the 330 // removed item was above the currently playing item). 331 if (index < fCurrentIndex) 332 SetCurrentItemIndex(fCurrentIndex - 1, false); 333 else if (index == fCurrentIndex) { 334 if (fCurrentIndex == CountItems()) 335 fCurrentIndex--; 336 SetCurrentItemIndex(fCurrentIndex, true); 337 } 338 } 339 340 return item; 341 } 342 343 344 int32 345 Playlist::IndexOf(PlaylistItem* item) const 346 { 347 return fItems.IndexOf(item); 348 } 349 350 351 PlaylistItem* 352 Playlist::ItemAt(int32 index) const 353 { 354 return (PlaylistItem*)fItems.ItemAt(index); 355 } 356 357 358 PlaylistItem* 359 Playlist::ItemAtFast(int32 index) const 360 { 361 return (PlaylistItem*)fItems.ItemAtFast(index); 362 } 363 364 365 // #pragma mark - navigation 366 367 368 bool 369 Playlist::SetCurrentItemIndex(int32 index, bool notify) 370 { 371 bool result = true; 372 if (index >= CountItems()) { 373 index = CountItems() - 1; 374 result = false; 375 notify = false; 376 } 377 if (index < 0) { 378 index = -1; 379 result = false; 380 } 381 if (index == fCurrentIndex && !notify) 382 return result; 383 384 fCurrentIndex = index; 385 _NotifyCurrentItemChanged(fCurrentIndex, notify); 386 return result; 387 } 388 389 390 int32 391 Playlist::CurrentItemIndex() const 392 { 393 return fCurrentIndex; 394 } 395 396 397 void 398 Playlist::GetSkipInfo(bool* canSkipPrevious, bool* canSkipNext) const 399 { 400 if (canSkipPrevious) 401 *canSkipPrevious = fCurrentIndex > 0; 402 if (canSkipNext) 403 *canSkipNext = fCurrentIndex < CountItems() - 1; 404 } 405 406 407 // pragma mark - 408 409 410 bool 411 Playlist::AddListener(Listener* listener) 412 { 413 BAutolock _(this); 414 if (listener && !fListeners.HasItem(listener)) 415 return fListeners.AddItem(listener); 416 return false; 417 } 418 419 420 void 421 Playlist::RemoveListener(Listener* listener) 422 { 423 BAutolock _(this); 424 fListeners.RemoveItem(listener); 425 } 426 427 428 // #pragma mark - support 429 430 431 void 432 Playlist::AppendRefs(const BMessage* refsReceivedMessage, int32 appendIndex) 433 { 434 // the playlist is replaced by the refs in the message 435 // or the refs are appended at the appendIndex 436 // in the existing playlist 437 if (appendIndex == APPEND_INDEX_APPEND_LAST) 438 appendIndex = CountItems(); 439 440 bool add = appendIndex != APPEND_INDEX_REPLACE_PLAYLIST; 441 442 if (!add) 443 MakeEmpty(); 444 445 bool startPlaying = CountItems() == 0; 446 447 Playlist temporaryPlaylist; 448 Playlist* playlist = add ? &temporaryPlaylist : this; 449 bool sortPlaylist = true; 450 451 entry_ref ref; 452 int32 subAppendIndex = CountItems(); 453 for (int i = 0; refsReceivedMessage->FindRef("refs", i, &ref) == B_OK; 454 i++) { 455 Playlist subPlaylist; 456 BString type = _MIMEString(&ref); 457 458 if (_IsPlaylist(type)) { 459 AppendPlaylistToPlaylist(ref, &subPlaylist); 460 // Do not sort the whole playlist anymore, as that 461 // will screw up the ordering in the saved playlist. 462 sortPlaylist = false; 463 } else { 464 if (_IsQuery(type)) 465 AppendQueryToPlaylist(ref, &subPlaylist); 466 else { 467 if (!ExtraMediaExists(this, ref)) { 468 AppendToPlaylistRecursive(ref, &subPlaylist); 469 } 470 } 471 472 // At least sort this subsection of the playlist 473 // if the whole playlist is not sorted anymore. 474 if (!sortPlaylist) 475 subPlaylist.Sort(); 476 } 477 478 if (!subPlaylist.IsEmpty()) { 479 // Add to recent documents 480 be_roster->AddToRecentDocuments(&ref, kAppSig); 481 } 482 483 int32 subPlaylistCount = subPlaylist.CountItems(); 484 AdoptPlaylist(subPlaylist, subAppendIndex); 485 subAppendIndex += subPlaylistCount; 486 } 487 if (sortPlaylist) 488 playlist->Sort(); 489 490 if (add) 491 AdoptPlaylist(temporaryPlaylist, appendIndex); 492 493 if (startPlaying) { 494 // open first file 495 SetCurrentItemIndex(0); 496 } 497 } 498 499 500 /*static*/ void 501 Playlist::AppendToPlaylistRecursive(const entry_ref& ref, Playlist* playlist) 502 { 503 // recursively append the ref (dive into folders) 504 BEntry entry(&ref, true); 505 if (entry.InitCheck() != B_OK || !entry.Exists()) 506 return; 507 508 if (entry.IsDirectory()) { 509 BDirectory dir(&entry); 510 if (dir.InitCheck() != B_OK) 511 return; 512 513 entry.Unset(); 514 515 entry_ref subRef; 516 while (dir.GetNextRef(&subRef) == B_OK) { 517 AppendToPlaylistRecursive(subRef, playlist); 518 } 519 } else if (entry.IsFile()) { 520 BString mimeString = _MIMEString(&ref); 521 if (_IsMediaFile(mimeString)) { 522 PlaylistItem* item = new (std::nothrow) FilePlaylistItem(ref); 523 if (!ExtraMediaExists(playlist, ref)) { 524 _BindExtraMedia(item); 525 if (item != NULL && !playlist->AddItem(item)) 526 delete item; 527 } else 528 delete item; 529 } else 530 printf("MIME Type = %s\n", mimeString.String()); 531 } 532 } 533 534 535 /*static*/ void 536 Playlist::AppendPlaylistToPlaylist(const entry_ref& ref, Playlist* playlist) 537 { 538 BEntry entry(&ref, true); 539 if (entry.InitCheck() != B_OK || !entry.Exists()) 540 return; 541 542 BString mimeString = _MIMEString(&ref); 543 if (_IsTextPlaylist(mimeString)) { 544 //printf("RunPlaylist thing\n"); 545 BFile file(&ref, B_READ_ONLY); 546 FileReadWrite lineReader(&file); 547 548 BString str; 549 entry_ref refPath; 550 status_t err; 551 BPath path; 552 while (lineReader.Next(str)) { 553 str = str.RemoveFirst("file://"); 554 str = str.RemoveLast(".."); 555 path = BPath(str.String()); 556 printf("Line %s\n", path.Path()); 557 if (path.Path() != NULL) { 558 if ((err = get_ref_for_path(path.Path(), &refPath)) == B_OK) { 559 PlaylistItem* item 560 = new (std::nothrow) FilePlaylistItem(refPath); 561 if (item == NULL || !playlist->AddItem(item)) 562 delete item; 563 } else { 564 printf("Error - %s: [%" B_PRIx32 "]\n", strerror(err), 565 err); 566 } 567 } else 568 printf("Error - No File Found in playlist\n"); 569 } 570 } else if (_IsBinaryPlaylist(mimeString)) { 571 BFile file(&ref, B_READ_ONLY); 572 Playlist temp; 573 if (temp.Unflatten(&file) == B_OK) 574 playlist->AdoptPlaylist(temp, playlist->CountItems()); 575 } 576 } 577 578 579 /*static*/ void 580 Playlist::AppendQueryToPlaylist(const entry_ref& ref, Playlist* playlist) 581 { 582 BQueryFile query(&ref); 583 if (query.InitCheck() != B_OK) 584 return; 585 586 entry_ref foundRef; 587 while (query.GetNextRef(&foundRef) == B_OK) { 588 PlaylistItem* item = new (std::nothrow) FilePlaylistItem(foundRef); 589 if (item == NULL || !playlist->AddItem(item)) 590 delete item; 591 } 592 } 593 594 595 void 596 Playlist::NotifyImportFailed() 597 { 598 BAutolock _(this); 599 _NotifyImportFailed(); 600 } 601 602 603 /*static*/ bool 604 Playlist::ExtraMediaExists(Playlist* playlist, const entry_ref& ref) 605 { 606 BString exceptExtension = _GetExceptExtension(BPath(&ref).Path()); 607 608 for (int32 i = 0; i < playlist->CountItems(); i++) { 609 FilePlaylistItem* compare = dynamic_cast<FilePlaylistItem*>(playlist->ItemAt(i)); 610 if (compare == NULL) 611 continue; 612 if (compare->Ref() != ref 613 && _GetExceptExtension(BPath(&compare->Ref()).Path()) == exceptExtension ) 614 return true; 615 } 616 return false; 617 } 618 619 620 // #pragma mark - private 621 622 623 /*static*/ bool 624 Playlist::_IsImageFile(const BString& mimeString) 625 { 626 BMimeType superType; 627 BMimeType fileType(mimeString.String()); 628 629 if (fileType.GetSupertype(&superType) != B_OK) 630 return false; 631 632 if (superType == "image") 633 return true; 634 635 return false; 636 } 637 638 639 /*static*/ bool 640 Playlist::_IsMediaFile(const BString& mimeString) 641 { 642 BMimeType superType; 643 BMimeType fileType(mimeString.String()); 644 645 if (fileType.GetSupertype(&superType) != B_OK) 646 return false; 647 648 // try a shortcut first 649 if (superType == "audio" || superType == "video") 650 return true; 651 652 // Look through our supported types 653 app_info appInfo; 654 if (be_app->GetAppInfo(&appInfo) != B_OK) 655 return false; 656 BFile appFile(&appInfo.ref, B_READ_ONLY); 657 if (appFile.InitCheck() != B_OK) 658 return false; 659 BMessage types; 660 BAppFileInfo appFileInfo(&appFile); 661 if (appFileInfo.GetSupportedTypes(&types) != B_OK) 662 return false; 663 664 const char* type; 665 for (int32 i = 0; types.FindString("types", i, &type) == B_OK; i++) { 666 if (strcasecmp(mimeString.String(), type) == 0) 667 return true; 668 } 669 670 return false; 671 } 672 673 674 /*static*/ bool 675 Playlist::_IsTextPlaylist(const BString& mimeString) 676 { 677 return mimeString.Compare(kTextPlaylistMimeString) == 0; 678 } 679 680 681 /*static*/ bool 682 Playlist::_IsBinaryPlaylist(const BString& mimeString) 683 { 684 return mimeString.Compare(kBinaryPlaylistMimeString) == 0; 685 } 686 687 688 /*static*/ bool 689 Playlist::_IsPlaylist(const BString& mimeString) 690 { 691 return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString); 692 } 693 694 695 /*static*/ bool 696 Playlist::_IsQuery(const BString& mimeString) 697 { 698 return mimeString.Compare(BQueryFile::MimeType()) == 0; 699 } 700 701 702 /*static*/ BString 703 Playlist::_MIMEString(const entry_ref* ref) 704 { 705 BFile file(ref, B_READ_ONLY); 706 BNodeInfo nodeInfo(&file); 707 char mimeString[B_MIME_TYPE_LENGTH]; 708 if (nodeInfo.GetType(mimeString) != B_OK) { 709 BMimeType type; 710 if (BMimeType::GuessMimeType(ref, &type) != B_OK) 711 return BString(); 712 713 strlcpy(mimeString, type.Type(), B_MIME_TYPE_LENGTH); 714 nodeInfo.SetType(type.Type()); 715 } 716 return BString(mimeString); 717 } 718 719 720 // _BindExtraMedia() searches additional videos and audios 721 // and addes them as extra medias. 722 /*static*/ void 723 Playlist::_BindExtraMedia(PlaylistItem* item) 724 { 725 FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item); 726 if (!fileItem) 727 return; 728 729 // If the media file is foo.mp3, _BindExtraMedia() searches foo.avi. 730 BPath mediaFilePath(&fileItem->Ref()); 731 BString mediaFilePathString = mediaFilePath.Path(); 732 BPath dirPath; 733 mediaFilePath.GetParent(&dirPath); 734 BDirectory dir(dirPath.Path()); 735 if (dir.InitCheck() != B_OK) 736 return; 737 738 BEntry entry; 739 BString entryPathString; 740 while (dir.GetNextEntry(&entry, true) == B_OK) { 741 if (!entry.IsFile()) 742 continue; 743 entryPathString = BPath(&entry).Path(); 744 if (entryPathString != mediaFilePathString 745 && _GetExceptExtension(entryPathString) == _GetExceptExtension(mediaFilePathString)) { 746 _BindExtraMedia(fileItem, entry); 747 } 748 } 749 } 750 751 752 /*static*/ void 753 Playlist::_BindExtraMedia(FilePlaylistItem* fileItem, const BEntry& entry) 754 { 755 entry_ref ref; 756 entry.GetRef(&ref); 757 BString mimeString = _MIMEString(&ref); 758 if (_IsMediaFile(mimeString)) { 759 fileItem->AddRef(ref); 760 } else if (_IsImageFile(mimeString)) { 761 fileItem->AddImageRef(ref); 762 } 763 } 764 765 766 /*static*/ BString 767 Playlist::_GetExceptExtension(const BString& path) 768 { 769 int32 periodPos = path.FindLast('.'); 770 if (periodPos <= path.FindLast('/')) 771 return path; 772 return BString(path.String(), periodPos); 773 } 774 775 776 // #pragma mark - notifications 777 778 779 void 780 Playlist::_NotifyItemAdded(PlaylistItem* item, int32 index) const 781 { 782 BList listeners(fListeners); 783 int32 count = listeners.CountItems(); 784 for (int32 i = 0; i < count; i++) { 785 Listener* listener = (Listener*)listeners.ItemAtFast(i); 786 listener->ItemAdded(item, index); 787 } 788 } 789 790 791 void 792 Playlist::_NotifyItemRemoved(int32 index) const 793 { 794 BList listeners(fListeners); 795 int32 count = listeners.CountItems(); 796 for (int32 i = 0; i < count; i++) { 797 Listener* listener = (Listener*)listeners.ItemAtFast(i); 798 listener->ItemRemoved(index); 799 } 800 } 801 802 803 void 804 Playlist::_NotifyItemsSorted() const 805 { 806 BList listeners(fListeners); 807 int32 count = listeners.CountItems(); 808 for (int32 i = 0; i < count; i++) { 809 Listener* listener = (Listener*)listeners.ItemAtFast(i); 810 listener->ItemsSorted(); 811 } 812 } 813 814 815 void 816 Playlist::_NotifyCurrentItemChanged(int32 newIndex, bool play) const 817 { 818 BList listeners(fListeners); 819 int32 count = listeners.CountItems(); 820 for (int32 i = 0; i < count; i++) { 821 Listener* listener = (Listener*)listeners.ItemAtFast(i); 822 listener->CurrentItemChanged(newIndex, play); 823 } 824 } 825 826 827 void 828 Playlist::_NotifyImportFailed() const 829 { 830 BList listeners(fListeners); 831 int32 count = listeners.CountItems(); 832 for (int32 i = 0; i < count; i++) { 833 Listener* listener = (Listener*)listeners.ItemAtFast(i); 834 listener->ImportFailed(); 835 } 836 } 837