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