⛏️ index : haiku.git

/*
 * Playlist.cpp - Media Player for the Haiku Operating System
 *
 * Copyright (C) 2006 Marcus Overhagen <marcus@overhagen.de>
 * Copyright (C) 2007-2009 Stephan Aßmus <superstippi@gmx.de> (MIT ok)
 * Copyright (C) 2008-2009 Fredrik Modéen <[FirstName]@[LastName].se> (MIT ok)
 *
 * Released under the terms of the MIT license.
 */


#include "Playlist.h"

#include <debugger.h>
#include <new>
#include <stdio.h>
#include <strings.h>

#include <AppFileInfo.h>
#include <Application.h>
#include <Autolock.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <Message.h>
#include <Mime.h>
#include <NodeInfo.h>
#include <Path.h>
#include <Roster.h>
#include <String.h>

#include <QueryFile.h>

#include "FilePlaylistItem.h"
#include "FileReadWrite.h"
#include "MainApp.h"

using std::nothrow;

// TODO: using BList for objects is bad, replace it with a template

Playlist::Listener::Listener() {}
Playlist::Listener::~Listener() {}
void Playlist::Listener::ItemAdded(PlaylistItem* item, int32 index) {}
void Playlist::Listener::ItemRemoved(int32 index) {}
void Playlist::Listener::ItemsSorted() {}
void Playlist::Listener::CurrentItemChanged(int32 newIndex, bool play) {}
void Playlist::Listener::ImportFailed() {}


// #pragma mark -


static void
make_item_compare_string(const PlaylistItem* item, char* buffer,
	size_t bufferSize)
{
	// TODO: Maybe "location" would be useful here as well.
//	snprintf(buffer, bufferSize, "%s - %s - %0*ld - %s",
//		item->Author().String(),
//		item->Album().String(),
//		3, item->TrackNumber(),
//		item->Title().String());
	snprintf(buffer, bufferSize, "%s", item->LocationURI().String());
}


static int
playlist_item_compare(const void* _item1, const void* _item2)
{
	// compare complete path
	const PlaylistItem* item1 = *(const PlaylistItem**)_item1;
	const PlaylistItem* item2 = *(const PlaylistItem**)_item2;

	static const size_t bufferSize = 1024;
	char string1[bufferSize];
	make_item_compare_string(item1, string1, bufferSize);
	char string2[bufferSize];
	make_item_compare_string(item2, string2, bufferSize);

	return strcmp(string1, string2);
}


// #pragma mark -


Playlist::Playlist()
	:
	BLocker("playlist lock"),
	fItems(),
 	fCurrentIndex(-1)
{
}


Playlist::~Playlist()
{
	MakeEmpty();

	if (fListeners.CountItems() > 0)
		debugger("Playlist::~Playlist() - there are still listeners attached!");
}


// #pragma mark - archiving


static const char* kItemArchiveKey = "item";


status_t
Playlist::Unarchive(const BMessage* archive)
{
	if (archive == NULL)
		return B_BAD_VALUE;

	MakeEmpty();

	BMessage itemArchive;
	for (int32 i = 0;
		archive->FindMessage(kItemArchiveKey, i, &itemArchive) == B_OK; i++) {

		BArchivable* archivable = instantiate_object(&itemArchive);
		PlaylistItem* item = dynamic_cast<PlaylistItem*>(archivable);
		if (!item) {
			delete archivable;
			continue;
		}

		if (!AddItem(item)) {
			delete item;
			return B_NO_MEMORY;
		}
	}

	return B_OK;
}


status_t
Playlist::Archive(BMessage* into) const
{
	if (into == NULL)
		return B_BAD_VALUE;

	int32 count = CountItems();
	for (int32 i = 0; i < count; i++) {
		const PlaylistItem* item = ItemAtFast(i);
		BMessage itemArchive;
		status_t ret = item->Archive(&itemArchive);
		if (ret != B_OK)
			return ret;
		ret = into->AddMessage(kItemArchiveKey, &itemArchive);
		if (ret != B_OK)
			return ret;
	}

	return B_OK;
}


const uint32 kPlaylistMagicBytes = 'MPPL';
const char* kTextPlaylistMimeString = "text/x-playlist";
const char* kBinaryPlaylistMimeString = "application/x-vnd.haiku-playlist";

status_t
Playlist::Unflatten(BDataIO* stream)
{
	if (stream == NULL)
		return B_BAD_VALUE;

	uint32 magicBytes;
	ssize_t read = stream->Read(&magicBytes, 4);
	if (read != 4) {
		if (read < 0)
			return (status_t)read;
		return B_IO_ERROR;
	}

	if (B_LENDIAN_TO_HOST_INT32(magicBytes) != kPlaylistMagicBytes)
		return B_BAD_VALUE;

	BMessage archive;
	status_t ret = archive.Unflatten(stream);
	if (ret != B_OK)
		return ret;

	return Unarchive(&archive);
}


status_t
Playlist::Flatten(BDataIO* stream) const
{
	if (stream == NULL)
		return B_BAD_VALUE;

	BMessage archive;
	status_t ret = Archive(&archive);
	if (ret != B_OK)
		return ret;

	uint32 magicBytes = B_HOST_TO_LENDIAN_INT32(kPlaylistMagicBytes);
	ssize_t written = stream->Write(&magicBytes, 4);
	if (written != 4) {
		if (written < 0)
			return (status_t)written;
		return B_IO_ERROR;
	}

	return archive.Flatten(stream);
}


// #pragma mark - list access


void
Playlist::MakeEmpty(bool deleteItems)
{
	int32 count = CountItems();
	for (int32 i = count - 1; i >= 0; i--) {
		PlaylistItem* item = RemoveItem(i, false);
		_NotifyItemRemoved(i);
		if (deleteItems)
			item->ReleaseReference();
	}
	SetCurrentItemIndex(-1);
}


int32
Playlist::CountItems() const
{
	return fItems.CountItems();
}


bool
Playlist::IsEmpty() const
{
	return fItems.IsEmpty();
}


void
Playlist::Sort()
{
	fItems.SortItems(playlist_item_compare);
	_NotifyItemsSorted();
}


bool
Playlist::AddItem(PlaylistItem* item)
{
	return AddItem(item, CountItems());
}


bool
Playlist::AddItem(PlaylistItem* item, int32 index)
{
	if (!fItems.AddItem(item, index))
		return false;

	if (index <= fCurrentIndex)
		SetCurrentItemIndex(fCurrentIndex + 1, false);

	_NotifyItemAdded(item, index);

	return true;
}


bool
Playlist::AdoptPlaylist(Playlist& other)
{
	return AdoptPlaylist(other, CountItems());
}


bool
Playlist::AdoptPlaylist(Playlist& other, int32 index)
{
	if (&other == this)
		return false;
	// NOTE: this is not intended to merge two "equal" playlists
	// the given playlist is assumed to be a temporary "dummy"
	if (fItems.AddList(&other.fItems, index)) {
		// take care of the notifications
		int32 count = other.CountItems();
		for (int32 i = index; i < index + count; i++) {
			PlaylistItem* item = ItemAtFast(i);
			_NotifyItemAdded(item, i);
		}
		if (index <= fCurrentIndex)
			SetCurrentItemIndex(fCurrentIndex + count);
		// empty the other list, so that the PlaylistItems are now ours
		other.fItems.MakeEmpty();
		return true;
	}
	return false;
}


PlaylistItem*
Playlist::RemoveItem(int32 index, bool careAboutCurrentIndex)
{
	PlaylistItem* item = (PlaylistItem*)fItems.RemoveItem(index);
	if (!item)
		return NULL;
	_NotifyItemRemoved(index);

	if (careAboutCurrentIndex) {
		// fCurrentIndex isn't in sync yet, so might be one too large (if the
		// removed item was above the currently playing item).
		if (index < fCurrentIndex)
			SetCurrentItemIndex(fCurrentIndex - 1, false);
		else if (index == fCurrentIndex) {
			if (fCurrentIndex == CountItems())
				fCurrentIndex--;
			SetCurrentItemIndex(fCurrentIndex, true);
		}
	}

	return item;
}


int32
Playlist::IndexOf(PlaylistItem* item) const
{
	return fItems.IndexOf(item);
}


PlaylistItem*
Playlist::ItemAt(int32 index) const
{
	return (PlaylistItem*)fItems.ItemAt(index);
}


PlaylistItem*
Playlist::ItemAtFast(int32 index) const
{
	return (PlaylistItem*)fItems.ItemAtFast(index);
}


// #pragma mark - navigation


bool
Playlist::SetCurrentItemIndex(int32 index, bool notify)
{
	bool result = true;
	if (index >= CountItems()) {
		index = CountItems() - 1;
		result = false;
		notify = false;
	}
	if (index < 0) {
		index = -1;
		result = false;
	}
	if (index == fCurrentIndex && !notify)
		return result;

	fCurrentIndex = index;
	_NotifyCurrentItemChanged(fCurrentIndex, notify);
	return result;
}


int32
Playlist::CurrentItemIndex() const
{
	return fCurrentIndex;
}


void
Playlist::GetSkipInfo(bool* canSkipPrevious, bool* canSkipNext) const
{
	if (canSkipPrevious)
		*canSkipPrevious = fCurrentIndex > 0;
	if (canSkipNext)
		*canSkipNext = fCurrentIndex < CountItems() - 1;
}


// pragma mark -


bool
Playlist::AddListener(Listener* listener)
{
	BAutolock _(this);
	if (listener && !fListeners.HasItem(listener))
		return fListeners.AddItem(listener);
	return false;
}


void
Playlist::RemoveListener(Listener* listener)
{
	BAutolock _(this);
	fListeners.RemoveItem(listener);
}


// #pragma mark - support


void
Playlist::AppendItems(const BMessage* refsReceivedMessage, int32 appendIndex,
	bool sortItems)
{
	// the playlist is replaced by the refs in the message
	// or the refs are appended at the appendIndex
	// in the existing playlist
	if (appendIndex == APPEND_INDEX_APPEND_LAST)
		appendIndex = CountItems();

	bool add = appendIndex != APPEND_INDEX_REPLACE_PLAYLIST;

	if (!add)
		MakeEmpty();

	bool startPlaying = CountItems() == 0;

	Playlist temporaryPlaylist;
	Playlist* playlist = add ? &temporaryPlaylist : this;
	bool hasSavedPlaylist = false;

	// TODO: This is not very fair, we should abstract from
	// entry ref representation and support more URLs.
	BMessage archivedUrl;
	if (refsReceivedMessage->FindMessage("mediaplayer:url", &archivedUrl)
			== B_OK) {
		BUrl url(&archivedUrl);
		AddItem(new UrlPlaylistItem(url));
	}

	entry_ref ref;
	int32 subAppendIndex = CountItems();
	for (int i = 0; refsReceivedMessage->FindRef("refs", i, &ref) == B_OK;
			i++) {
		Playlist subPlaylist;
		BString type = _MIMEString(&ref);

		if (_IsPlaylist(type)) {
			AppendPlaylistToPlaylist(ref, &subPlaylist);
			// Do not sort the whole playlist anymore, as that
			// will screw up the ordering in the saved playlist.
			hasSavedPlaylist = true;
		} else {
			if (_IsQuery(type))
				AppendQueryToPlaylist(ref, &subPlaylist);
			else {
				PlaylistFileReader* reader = PlaylistFileReader::GenerateReader(ref);
				if (reader != NULL) {
					reader->AppendToPlaylist(ref, &subPlaylist);
					delete reader;
				} else {
					if (!_ExtraMediaExists(this, ref)) {
						AppendToPlaylistRecursive(ref, &subPlaylist);
					}
				}
			}

			// At least sort this subsection of the playlist
			// if the whole playlist is not sorted anymore.
			if (sortItems && hasSavedPlaylist)
				subPlaylist.Sort();
		}

		if (!subPlaylist.IsEmpty()) {
			// Add to recent documents
			be_roster->AddToRecentDocuments(&ref, kAppSig);
		}

		int32 subPlaylistCount = subPlaylist.CountItems();
		AdoptPlaylist(subPlaylist, subAppendIndex);
		subAppendIndex += subPlaylistCount;
	}

	if (sortItems)
		playlist->Sort();

	if (add)
		AdoptPlaylist(temporaryPlaylist, appendIndex);

	if (startPlaying) {
		// open first file
		SetCurrentItemIndex(0);
	}
}


/*static*/ void
Playlist::AppendToPlaylistRecursive(const entry_ref& ref, Playlist* playlist)
{
	// recursively append the ref (dive into folders)
	BEntry entry(&ref, true);
	if (entry.InitCheck() != B_OK || !entry.Exists())
		return;

	if (entry.IsDirectory()) {
		BDirectory dir(&entry);
		if (dir.InitCheck() != B_OK)
			return;

		entry.Unset();

		entry_ref subRef;
		while (dir.GetNextRef(&subRef) == B_OK) {
			AppendToPlaylistRecursive(subRef, playlist);
		}
	} else if (entry.IsFile()) {
		BString mimeString = _MIMEString(&ref);
		if (_IsMediaFile(mimeString, ref)) {
			PlaylistItem* item = new (std::nothrow) FilePlaylistItem(ref);
			if (!_ExtraMediaExists(playlist, ref)) {
				_BindExtraMedia(item);
				if (item != NULL && !playlist->AddItem(item))
					delete item;
			} else
				delete item;
		} else
			printf("MIME Type = %s\n", mimeString.String());
	}
}


/*static*/ void
Playlist::AppendPlaylistToPlaylist(const entry_ref& ref, Playlist* playlist)
{
	BEntry entry(&ref, true);
	if (entry.InitCheck() != B_OK || !entry.Exists())
		return;

	BString mimeString = _MIMEString(&ref);
	if (_IsTextPlaylist(mimeString)) {
		//printf("RunPlaylist thing\n");
		BFile file(&ref, B_READ_ONLY);
		FileReadWrite lineReader(&file);

		BString str;
		entry_ref refPath;
		status_t err;
		BPath path;
		while (lineReader.Next(str)) {
			str = str.RemoveFirst("file://");
			str = str.RemoveLast("..");
			path = BPath(str.String());
			printf("Line %s\n", path.Path());
			if (path.Path() != NULL) {
				if ((err = get_ref_for_path(path.Path(), &refPath)) == B_OK) {
					PlaylistItem* item
						= new (std::nothrow) FilePlaylistItem(refPath);
					if (item == NULL || !playlist->AddItem(item))
						delete item;
				} else {
					printf("Error - %s: [%" B_PRIx32 "]\n", strerror(err),
						err);
				}
			} else
				printf("Error - No File Found in playlist\n");
		}
	} else if (_IsBinaryPlaylist(mimeString)) {
		BFile file(&ref, B_READ_ONLY);
		Playlist temp;
		if (temp.Unflatten(&file) == B_OK)
			playlist->AdoptPlaylist(temp, playlist->CountItems());
	}
}


/*static*/ void
Playlist::AppendQueryToPlaylist(const entry_ref& ref, Playlist* playlist)
{
	BQueryFile query(&ref);
	if (query.InitCheck() != B_OK)
		return;

	entry_ref foundRef;
	while (query.GetNextRef(&foundRef) == B_OK) {
		PlaylistItem* item = new (std::nothrow) FilePlaylistItem(foundRef);
		if (item == NULL || !playlist->AddItem(item))
			delete item;
	}
}


void
Playlist::NotifyImportFailed()
{
	BAutolock _(this);
	_NotifyImportFailed();
}


/*static*/ bool
Playlist::ExtraMediaExists(Playlist* playlist, PlaylistItem* item)
{
	FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item);
	if (fileItem != NULL)
		return _ExtraMediaExists(playlist, fileItem->Ref());

	// If we are here let's see if it is an url
	UrlPlaylistItem* urlItem = dynamic_cast<UrlPlaylistItem*>(item);
	if (urlItem == NULL)
		return true;

	return _ExtraMediaExists(playlist, urlItem->Url());
}


// #pragma mark - private


/*static*/ bool
Playlist::_ExtraMediaExists(Playlist* playlist, const entry_ref& ref)
{
	BString exceptExtension = _GetExceptExtension(BPath(&ref).Path());

	for (int32 i = 0; i < playlist->CountItems(); i++) {
		FilePlaylistItem* compare = dynamic_cast<FilePlaylistItem*>(playlist->ItemAt(i));
		if (compare == NULL)
			continue;
		if (compare->Ref() != ref
				&& _GetExceptExtension(BPath(&compare->Ref()).Path()) == exceptExtension )
			return true;
	}
	return false;
}


/*static*/ bool
Playlist::_ExtraMediaExists(Playlist* playlist, BUrl url)
{
	for (int32 i = 0; i < playlist->CountItems(); i++) {
		UrlPlaylistItem* compare
			= dynamic_cast<UrlPlaylistItem*>(playlist->ItemAt(i));
		if (compare == NULL)
			continue;
		if (compare->Url() == url)
			return true;
	}
	return false;
}


/*static*/ bool
Playlist::_IsImageFile(const BString& mimeString)
{
	BMimeType superType;
	BMimeType fileType(mimeString.String());

	if (fileType.GetSupertype(&superType) != B_OK)
		return false;

	if (superType == "image")
		return true;

	return false;
}


/*static*/ bool
Playlist::_IsMediaFile(const BString& mimeString, const entry_ref& ref)
{
	BMimeType superType;
	BMimeType fileType(mimeString.String());

	if (fileType.GetSupertype(&superType) != B_OK) {
		// no superType, try to sniff the type
		if (BMimeType::GuessMimeType(&ref, &fileType) != B_OK)
			return false;
	}

	BEntry entry(&ref);
	if (entry.IsSymLink()) {
		// traverse link
		entry_ref* fileRef = const_cast<entry_ref*>(&ref);
		if (entry.SetTo(fileRef, true) != B_OK)
			return false;
		if (entry.GetRef(fileRef) != B_OK)
			return false;
		fileType.SetTo(_MIMEString(fileRef));
	}

	if (fileType.GetSupertype(&superType) != B_OK)
		return false;

	// try a shortcut first
	if (superType == "audio" || superType == "video")
		return true;

	// Look through our supported types
	app_info appInfo;
	if (be_app->GetAppInfo(&appInfo) != B_OK)
		return false;
	BFile appFile(&appInfo.ref, B_READ_ONLY);
	if (appFile.InitCheck() != B_OK)
		return false;
	BMessage types;
	BAppFileInfo appFileInfo(&appFile);
	if (appFileInfo.GetSupportedTypes(&types) != B_OK)
		return false;

	const char* type;
	for (int32 i = 0; types.FindString("types", i, &type) == B_OK; i++) {
		if (strcasecmp(fileType.Type(), type) == 0)
			return true;
	}

	return false;
}


/*static*/ bool
Playlist::_IsTextPlaylist(const BString& mimeString)
{
	return mimeString.Compare(kTextPlaylistMimeString) == 0;
}


/*static*/ bool
Playlist::_IsBinaryPlaylist(const BString& mimeString)
{
	return mimeString.Compare(kBinaryPlaylistMimeString) == 0;
}


/*static*/ bool
Playlist::_IsPlaylist(const BString& mimeString)
{
	return _IsTextPlaylist(mimeString) || _IsBinaryPlaylist(mimeString);
}


/*static*/ bool
Playlist::_IsQuery(const BString& mimeString)
{
	return mimeString.Compare(BQueryFile::MimeType()) == 0;
}


/*static*/ BString
Playlist::_MIMEString(const entry_ref* ref)
{
	BFile file(ref, B_READ_ONLY);
	BNodeInfo nodeInfo(&file);
	char mimeString[B_MIME_TYPE_LENGTH];
	if (nodeInfo.GetType(mimeString) != B_OK) {
		BMimeType type;
		if (BMimeType::GuessMimeType(ref, &type) != B_OK)
			return BString();

		strlcpy(mimeString, type.Type(), B_MIME_TYPE_LENGTH);
		nodeInfo.SetType(type.Type());
	}
	return BString(mimeString);
}


// _BindExtraMedia() searches additional videos and audios
// and addes them as extra medias.
/*static*/ void
Playlist::_BindExtraMedia(PlaylistItem* item)
{
	FilePlaylistItem* fileItem = dynamic_cast<FilePlaylistItem*>(item);
	if (!fileItem)
		return;

	// If the media file is foo.mp3, _BindExtraMedia() searches foo.avi.
	BPath mediaFilePath(&fileItem->Ref());
	BString mediaFilePathString = mediaFilePath.Path();
	BPath dirPath;
	mediaFilePath.GetParent(&dirPath);
	BDirectory dir(dirPath.Path());
	if (dir.InitCheck() != B_OK)
		return;

	BEntry entry;
	BString entryPathString;
	while (dir.GetNextEntry(&entry, true) == B_OK) {
		if (!entry.IsFile())
			continue;
		entryPathString = BPath(&entry).Path();
		if (entryPathString != mediaFilePathString
				&& _GetExceptExtension(entryPathString) == _GetExceptExtension(mediaFilePathString)) {
			_BindExtraMedia(fileItem, entry);
		}
	}
}


/*static*/ void
Playlist::_BindExtraMedia(FilePlaylistItem* fileItem, const BEntry& entry)
{
	entry_ref ref;
	entry.GetRef(&ref);
	BString mimeString = _MIMEString(&ref);
	if (_IsMediaFile(mimeString, ref)) {
		fileItem->AddRef(ref);
	} else if (_IsImageFile(mimeString)) {
		fileItem->AddImageRef(ref);
	}
}


/*static*/ BString
Playlist::_GetExceptExtension(const BString& path)
{
	int32 periodPos = path.FindLast('.');
	if (periodPos <= path.FindLast('/'))
		return path;
	return BString(path.String(), periodPos);
}


// #pragma mark - notifications


void
Playlist::_NotifyItemAdded(PlaylistItem* item, int32 index) const
{
	BList listeners(fListeners);
	int32 count = listeners.CountItems();
	for (int32 i = 0; i < count; i++) {
		Listener* listener = (Listener*)listeners.ItemAtFast(i);
		listener->ItemAdded(item, index);
	}
}


void
Playlist::_NotifyItemRemoved(int32 index) const
{
	BList listeners(fListeners);
	int32 count = listeners.CountItems();
	for (int32 i = 0; i < count; i++) {
		Listener* listener = (Listener*)listeners.ItemAtFast(i);
		listener->ItemRemoved(index);
	}
}


void
Playlist::_NotifyItemsSorted() const
{
	BList listeners(fListeners);
	int32 count = listeners.CountItems();
	for (int32 i = 0; i < count; i++) {
		Listener* listener = (Listener*)listeners.ItemAtFast(i);
		listener->ItemsSorted();
	}
}


void
Playlist::_NotifyCurrentItemChanged(int32 newIndex, bool play) const
{
	BList listeners(fListeners);
	int32 count = listeners.CountItems();
	for (int32 i = 0; i < count; i++) {
		Listener* listener = (Listener*)listeners.ItemAtFast(i);
		listener->CurrentItemChanged(newIndex, play);
	}
}


void
Playlist::_NotifyImportFailed() const
{
	BList listeners(fListeners);
	int32 count = listeners.CountItems();
	for (int32 i = 0; i < count; i++) {
		Listener* listener = (Listener*)listeners.ItemAtFast(i);
		listener->ImportFailed();
	}
}