diff options
Diffstat (limited to 'xbmc/playlists')
28 files changed, 4454 insertions, 0 deletions
diff --git a/xbmc/playlists/CMakeLists.txt b/xbmc/playlists/CMakeLists.txt new file mode 100644 index 0000000..c665156 --- /dev/null +++ b/xbmc/playlists/CMakeLists.txt @@ -0,0 +1,26 @@ +set(SOURCES PlayListB4S.cpp + PlayList.cpp + PlayListFactory.cpp + PlayListM3U.cpp + PlayListPLS.cpp + PlayListURL.cpp + PlayListWPL.cpp + PlayListXML.cpp + PlayListXSPF.cpp + SmartPlayList.cpp + SmartPlaylistFileItemListModifier.cpp) + +set(HEADERS PlayList.h + PlayListB4S.h + PlayListFactory.h + PlayListM3U.h + PlayListPLS.h + PlayListTypes.h + PlayListURL.h + PlayListWPL.h + PlayListXML.h + PlayListXSPF.h + SmartPlayList.h + SmartPlaylistFileItemListModifier.h) + +core_add_library(playlists) diff --git a/xbmc/playlists/PlayList.cpp b/xbmc/playlists/PlayList.cpp new file mode 100644 index 0000000..7c03884 --- /dev/null +++ b/xbmc/playlists/PlayList.cpp @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayList.h" + +#include "FileItem.h" +#include "PlayListFactory.h" +#include "ServiceBroker.h" +#include "filesystem/File.h" +#include "interfaces/AnnouncementManager.h" +#include "music/tags/MusicInfoTag.h" +#include "utils/Random.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <cassert> +#include <iostream> +#include <sstream> +#include <string> +#include <utility> +#include <vector> + + +using namespace MUSIC_INFO; +using namespace XFILE; +using namespace PLAYLIST; + +CPlayList::CPlayList(Id id /* = PLAYLIST::TYPE_NONE */) : m_id(id) +{ + m_iPlayableItems = -1; + m_bShuffled = false; + m_bWasPlayed = false; +} + +void CPlayList::AnnounceRemove(int pos) +{ + if (m_id == TYPE_NONE) + return; + + CVariant data; + data["playlistid"] = m_id; + data["position"] = pos; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnRemove", data); +} + +void CPlayList::AnnounceClear() +{ + if (m_id == TYPE_NONE) + return; + + CVariant data; + data["playlistid"] = m_id; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnClear", data); +} + +void CPlayList::AnnounceAdd(const std::shared_ptr<CFileItem>& item, int pos) +{ + if (m_id == TYPE_NONE) + return; + + CVariant data; + data["playlistid"] = m_id; + data["position"] = pos; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnAdd", item, data); +} + +void CPlayList::Add(const std::shared_ptr<CFileItem>& item, int iPosition, int iOrder) +{ + int iOldSize = size(); + if (iPosition < 0 || iPosition >= iOldSize) + iPosition = iOldSize; + if (iOrder < 0 || iOrder >= iOldSize) + item->m_iprogramCount = iOldSize; + else + item->m_iprogramCount = iOrder; + + // increment the playable counter + item->ClearProperty("unplayable"); + if (m_iPlayableItems < 0) + m_iPlayableItems = 1; + else + m_iPlayableItems++; + + // set 'IsPlayable' property - needed for properly handling plugin:// URLs + item->SetProperty("IsPlayable", true); + + //CLog::Log(LOGDEBUG,"{} item:({:02}/{:02})[{}]", __FUNCTION__, iPosition, item->m_iprogramCount, item->GetPath()); + if (iPosition == iOldSize) + m_vecItems.push_back(item); + else + { + ivecItems it = m_vecItems.begin() + iPosition; + m_vecItems.insert(it, 1, item); + // correct any duplicate order values + if (iOrder < iOldSize) + IncrementOrder(iPosition + 1, iOrder); + } + AnnounceAdd(item, iPosition); +} + +void CPlayList::Add(const std::shared_ptr<CFileItem>& item) +{ + Add(item, -1, -1); +} + +void CPlayList::Add(const CPlayList& playlist) +{ + for (int i = 0; i < playlist.size(); i++) + Add(playlist[i], -1, -1); +} + +void CPlayList::Add(const CFileItemList& items) +{ + for (int i = 0; i < items.Size(); i++) + Add(items[i]); +} + +void CPlayList::Insert(const CPlayList& playlist, int iPosition /* = -1 */) +{ + // out of bounds so just add to the end + int iSize = size(); + if (iPosition < 0 || iPosition >= iSize) + { + Add(playlist); + return; + } + for (int i = 0; i < playlist.size(); i++) + { + int iPos = iPosition + i; + Add(playlist[i], iPos, iPos); + } +} + +void CPlayList::Insert(const CFileItemList& items, int iPosition /* = -1 */) +{ + // out of bounds so just add to the end + int iSize = size(); + if (iPosition < 0 || iPosition >= iSize) + { + Add(items); + return; + } + for (int i = 0; i < items.Size(); i++) + { + Add(items[i], iPosition + i, iPosition + i); + } +} + +void CPlayList::Insert(const std::shared_ptr<CFileItem>& item, int iPosition /* = -1 */) +{ + // out of bounds so just add to the end + int iSize = size(); + if (iPosition < 0 || iPosition >= iSize) + { + Add(item); + return; + } + Add(item, iPosition, iPosition); +} + +void CPlayList::DecrementOrder(int iOrder) +{ + if (iOrder < 0) return; + + // it was the last item so do nothing + if (iOrder == size()) return; + + // fix all items with an order greater than the removed iOrder + ivecItems it; + it = m_vecItems.begin(); + while (it != m_vecItems.end()) + { + CFileItemPtr item = *it; + if (item->m_iprogramCount > iOrder) + { + //CLog::Log(LOGDEBUG,"{} fixing item at order {}", __FUNCTION__, item->m_iprogramCount); + item->m_iprogramCount--; + } + ++it; + } +} + +void CPlayList::IncrementOrder(int iPosition, int iOrder) +{ + if (iOrder < 0) return; + + // fix all items with an order equal or greater to the added iOrder at iPos + ivecItems it; + it = m_vecItems.begin() + iPosition; + while (it != m_vecItems.end()) + { + CFileItemPtr item = *it; + if (item->m_iprogramCount >= iOrder) + { + //CLog::Log(LOGDEBUG,"{} fixing item at order {}", __FUNCTION__, item->m_iprogramCount); + item->m_iprogramCount++; + } + ++it; + } +} + +void CPlayList::Clear() +{ + bool announce = false; + if (!m_vecItems.empty()) + { + m_vecItems.erase(m_vecItems.begin(), m_vecItems.end()); + announce = true; + } + m_strPlayListName = ""; + m_iPlayableItems = -1; + m_bWasPlayed = false; + + if (announce) + AnnounceClear(); +} + +int CPlayList::size() const +{ + return (int)m_vecItems.size(); +} + +const std::shared_ptr<CFileItem> CPlayList::operator[](int iItem) const +{ + if (iItem < 0 || iItem >= size()) + { + assert(false); + CLog::Log(LOGERROR, "Error trying to retrieve an item that's out of range"); + return CFileItemPtr(); + } + return m_vecItems[iItem]; +} + +std::shared_ptr<CFileItem> CPlayList::operator[](int iItem) +{ + if (iItem < 0 || iItem >= size()) + { + assert(false); + CLog::Log(LOGERROR, "Error trying to retrieve an item that's out of range"); + return CFileItemPtr(); + } + return m_vecItems[iItem]; +} + +void CPlayList::Shuffle(int iPosition) +{ + if (size() == 0) + // nothing to shuffle, just set the flag for later + m_bShuffled = true; + else + { + if (iPosition >= size()) + return; + if (iPosition < 0) + iPosition = 0; + CLog::Log(LOGDEBUG, "{} shuffling at pos:{}", __FUNCTION__, iPosition); + + ivecItems it = m_vecItems.begin() + iPosition; + KODI::UTILS::RandomShuffle(it, m_vecItems.end()); + + // the list is now shuffled! + m_bShuffled = true; + } +} + +struct SSortPlayListItem +{ + static bool PlaylistSort(const CFileItemPtr &left, const CFileItemPtr &right) + { + return (left->m_iprogramCount < right->m_iprogramCount); + } +}; + +void CPlayList::UnShuffle() +{ + std::sort(m_vecItems.begin(), m_vecItems.end(), SSortPlayListItem::PlaylistSort); + // the list is now unshuffled! + m_bShuffled = false; +} + +const std::string& CPlayList::GetName() const +{ + return m_strPlayListName; +} + +void CPlayList::Remove(const std::string& strFileName) +{ + int iOrder = -1; + int position = 0; + ivecItems it; + it = m_vecItems.begin(); + while (it != m_vecItems.end() ) + { + CFileItemPtr item = *it; + if (item->GetPath() == strFileName) + { + iOrder = item->m_iprogramCount; + it = m_vecItems.erase(it); + AnnounceRemove(position); + //CLog::Log(LOGDEBUG,"PLAYLIST, removing item at order {}", iPos); + } + else + { + ++position; + ++it; + } + } + DecrementOrder(iOrder); +} + +int CPlayList::FindOrder(int iOrder) const +{ + for (int i = 0; i < size(); i++) + { + if (m_vecItems[i]->m_iprogramCount == iOrder) + return i; + } + return -1; +} + +// remove item from playlist by position +void CPlayList::Remove(int position) +{ + int iOrder = -1; + if (position >= 0 && position < (int)m_vecItems.size()) + { + iOrder = m_vecItems[position]->m_iprogramCount; + m_vecItems.erase(m_vecItems.begin() + position); + } + DecrementOrder(iOrder); + + AnnounceRemove(position); +} + +int CPlayList::RemoveDVDItems() +{ + std::vector <std::string> vecFilenames; + + // Collect playlist items from DVD share + ivecItems it; + it = m_vecItems.begin(); + while (it != m_vecItems.end() ) + { + CFileItemPtr item = *it; + if ( item->IsCDDA() || item->IsOnDVD() ) + { + vecFilenames.push_back( item->GetPath() ); + } + ++it; + } + + // Delete them from playlist + int nFileCount = vecFilenames.size(); + if ( nFileCount ) + { + std::vector <std::string>::iterator it; + it = vecFilenames.begin(); + while (it != vecFilenames.end() ) + { + std::string& strFilename = *it; + Remove( strFilename ); + ++it; + } + vecFilenames.erase( vecFilenames.begin(), vecFilenames.end() ); + } + return nFileCount; +} + +bool CPlayList::Swap(int position1, int position2) +{ + if ( + (position1 < 0) || + (position2 < 0) || + (position1 >= size()) || + (position2 >= size()) + ) + { + return false; + } + + if (!IsShuffled()) + { + // swap the ordinals before swapping the items! + //CLog::Log(LOGDEBUG,"PLAYLIST swapping items at orders ({}, {})",m_vecItems[position1]->m_iprogramCount,m_vecItems[position2]->m_iprogramCount); + std::swap(m_vecItems[position1]->m_iprogramCount, m_vecItems[position2]->m_iprogramCount); + } + + // swap the items + std::swap(m_vecItems[position1], m_vecItems[position2]); + return true; +} + +void CPlayList::SetUnPlayable(int iItem) +{ + if (iItem < 0 || iItem >= size()) + { + CLog::Log(LOGWARNING, "Attempt to set unplayable index {}", iItem); + return; + } + + CFileItemPtr item = m_vecItems[iItem]; + if (!item->GetProperty("unplayable").asBoolean()) + { + item->SetProperty("unplayable", true); + m_iPlayableItems--; + } +} + + +bool CPlayList::Load(const std::string& strFileName) +{ + Clear(); + m_strBasePath = URIUtils::GetDirectory(strFileName); + + CFileStream file; + if (!file.Open(strFileName)) + return false; + + if (file.GetLength() > 1024*1024) + { + CLog::Log(LOGWARNING, "{} - File is larger than 1 MB, most likely not a playlist", + __FUNCTION__); + return false; + } + + return LoadData(file); +} + +bool CPlayList::LoadData(std::istream &stream) +{ + // try to read as a string + std::ostringstream ostr; + ostr << stream.rdbuf(); + return LoadData(ostr.str()); +} + +bool CPlayList::LoadData(const std::string& strData) +{ + return false; +} + + +bool CPlayList::Expand(int position) +{ + CFileItemPtr item = m_vecItems[position]; + std::unique_ptr<CPlayList> playlist (CPlayListFactory::Create(*item.get())); + if (playlist == nullptr) + return false; + + std::string path = item->GetDynPath(); + + if (!playlist->Load(path)) + return false; + + // remove any item that points back to itself + for (int i = 0;i<playlist->size();i++) + { + if (StringUtils::EqualsNoCase((*playlist)[i]->GetPath(), path)) + { + playlist->Remove(i); + i--; + } + } + + // @todo + // never change original path (id) of a file item + for (int i = 0;i<playlist->size();i++) + { + (*playlist)[i]->SetDynPath((*playlist)[i]->GetPath()); + (*playlist)[i]->SetPath(item->GetPath()); + (*playlist)[i]->SetStartOffset(item->GetStartOffset()); + } + + if (playlist->size() <= 0) + return false; + + Remove(position); + Insert(*playlist, position); + return true; +} + +void CPlayList::UpdateItem(const CFileItem *item) +{ + if (!item) return; + + for (ivecItems it = m_vecItems.begin(); it != m_vecItems.end(); ++it) + { + CFileItemPtr playlistItem = *it; + if (playlistItem->IsSamePath(item)) + { + std::string temp = playlistItem->GetPath(); // save path, it may have been altered + *playlistItem = *item; + playlistItem->SetPath(temp); + break; + } + } +} + +const std::string& CPlayList::ResolveURL(const std::shared_ptr<CFileItem>& item) const +{ + if (item->IsMusicDb() && item->HasMusicInfoTag()) + return item->GetMusicInfoTag()->GetURL(); + else + return item->GetDynPath(); +} diff --git a/xbmc/playlists/PlayList.h b/xbmc/playlists/PlayList.h new file mode 100644 index 0000000..8aa92be --- /dev/null +++ b/xbmc/playlists/PlayList.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayListTypes.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; +class CFileItemList; + +namespace PLAYLIST +{ + +class CPlayList +{ +public: + explicit CPlayList(PLAYLIST::Id id = PLAYLIST::TYPE_NONE); + virtual ~CPlayList(void) = default; + virtual bool Load(const std::string& strFileName); + virtual bool LoadData(std::istream &stream); + virtual bool LoadData(const std::string& strData); + virtual void Save(const std::string& strFileName) const {}; + + void Add(const CPlayList& playlist); + void Add(const std::shared_ptr<CFileItem>& pItem); + void Add(const CFileItemList& items); + + // for Party Mode + void Insert(const CPlayList& playlist, int iPosition = -1); + void Insert(const CFileItemList& items, int iPosition = -1); + void Insert(const std::shared_ptr<CFileItem>& item, int iPosition = -1); + + int FindOrder(int iOrder) const; + const std::string& GetName() const; + void Remove(const std::string& strFileName); + void Remove(int position); + bool Swap(int position1, int position2); + bool Expand(int position); // expands any playlist at position into this playlist + void Clear(); + int size() const; + int RemoveDVDItems(); + + const std::shared_ptr<CFileItem> operator[](int iItem) const; + std::shared_ptr<CFileItem> operator[](int iItem); + + void Shuffle(int iPosition = 0); + void UnShuffle(); + bool IsShuffled() const { return m_bShuffled; } + + void SetPlayed(bool bPlayed) { m_bWasPlayed = true; } + bool WasPlayed() const { return m_bWasPlayed; } + + void SetUnPlayable(int iItem); + int GetPlayable() const { return m_iPlayableItems; } + + void UpdateItem(const CFileItem *item); + + const std::string& ResolveURL(const std::shared_ptr<CFileItem>& item) const; + +protected: + PLAYLIST::Id m_id; + std::string m_strPlayListName; + std::string m_strBasePath; + int m_iPlayableItems; + bool m_bShuffled; + bool m_bWasPlayed; + +// CFileItemList m_vecItems; + std::vector<std::shared_ptr<CFileItem>> m_vecItems; + typedef std::vector<std::shared_ptr<CFileItem>>::iterator ivecItems; + +private: + void Add(const std::shared_ptr<CFileItem>& item, int iPosition, int iOrderOffset); + void DecrementOrder(int iOrder); + void IncrementOrder(int iPosition, int iOrder); + + void AnnounceRemove(int pos); + void AnnounceClear(); + void AnnounceAdd(const std::shared_ptr<CFileItem>& item, int pos); +}; + +typedef std::shared_ptr<CPlayList> CPlayListPtr; +} diff --git a/xbmc/playlists/PlayListB4S.cpp b/xbmc/playlists/PlayListB4S.cpp new file mode 100644 index 0000000..52a06fb --- /dev/null +++ b/xbmc/playlists/PlayListB4S.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListB4S.h" + +#include "FileItem.h" +#include "Util.h" +#include "filesystem/File.h" +#include "music/tags/MusicInfoTag.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <iostream> +#include <string> + +using namespace XFILE; +using namespace PLAYLIST; + +/* ------------------------ example b4s playlist file --------------------------------- + <?xml version="1.0" encoding='UTF-8' standalone="yes"?> + <WinampXML> + <!-- Generated by: Nullsoft Winamp3 version 3.0d --> + <playlist num_entries="2" label="Playlist 001"> + <entry Playstring="file:E:\Program Files\Winamp3\demo.mp3"> + <Name>demo</Name> + <Length>5982</Length> + </entry> + <entry Playstring="file:E:\Program Files\Winamp3\demo.mp3"> + <Name>demo</Name> + <Length>5982</Length> + </entry> + </playlist> + </WinampXML> +------------------------ end of example b4s playlist file ---------------------------------*/ +CPlayListB4S::CPlayListB4S(void) = default; + +CPlayListB4S::~CPlayListB4S(void) = default; + + +bool CPlayListB4S::LoadData(std::istream& stream) +{ + CXBMCTinyXML xmlDoc; + + stream >> xmlDoc; + + if (xmlDoc.Error()) + { + CLog::Log(LOGERROR, "Unable to parse B4S info Error: {}", xmlDoc.ErrorDesc()); + return false; + } + + TiXmlElement* pRootElement = xmlDoc.RootElement(); + if (!pRootElement ) return false; + + TiXmlElement* pPlayListElement = pRootElement->FirstChildElement("playlist"); + if (!pPlayListElement ) return false; + m_strPlayListName = XMLUtils::GetAttribute(pPlayListElement, "label"); + + TiXmlElement* pEntryElement = pPlayListElement->FirstChildElement("entry"); + + if (!pEntryElement) return false; + while (pEntryElement) + { + std::string strFileName = XMLUtils::GetAttribute(pEntryElement, "Playstring"); + size_t iColon = strFileName.find(':'); + if (iColon != std::string::npos) + { + iColon++; + strFileName.erase(0, iColon); + } + if (strFileName.size()) + { + TiXmlNode* pNodeInfo = pEntryElement->FirstChild("Name"); + TiXmlNode* pNodeLength = pEntryElement->FirstChild("Length"); + long lDuration = 0; + if (pNodeLength) + { + lDuration = atol(pNodeLength->FirstChild()->Value()); + } + if (pNodeInfo) + { + std::string strInfo = pNodeInfo->FirstChild()->Value(); + strFileName = URIUtils::SubstitutePath(strFileName); + CUtil::GetQualifiedFilename(m_strBasePath, strFileName); + CFileItemPtr newItem(new CFileItem(strInfo)); + newItem->SetPath(strFileName); + newItem->GetMusicInfoTag()->SetDuration(lDuration); + Add(newItem); + } + } + pEntryElement = pEntryElement->NextSiblingElement(); + } + return true; +} + +void CPlayListB4S::Save(const std::string& strFileName) const +{ + if (!m_vecItems.size()) return ; + std::string strPlaylist = strFileName; + strPlaylist = CUtil::MakeLegalPath(strPlaylist); + CFile file; + if (!file.OpenForWrite(strPlaylist, true)) + { + CLog::Log(LOGERROR, "Could not save B4S playlist: [{}]", strPlaylist); + return ; + } + std::string write; + write += StringUtils::Format("<?xml version={}1.0{} encoding='UTF-8' standalone={}yes{}?>\n", 34, + 34, 34, 34); + write += StringUtils::Format("<WinampXML>\n"); + write += StringUtils::Format(" <playlist num_entries=\"{0}\" label=\"{1}\">\n", + m_vecItems.size(), m_strPlayListName); + for (int i = 0; i < (int)m_vecItems.size(); ++i) + { + const CFileItemPtr item = m_vecItems[i]; + write += StringUtils::Format(" <entry Playstring={}file:{}{}>\n", 34, item->GetPath(), 34); + write += StringUtils::Format(" <Name>{}</Name>\n", item->GetLabel().c_str()); + write += + StringUtils::Format(" <Length>{}</Length>\n", item->GetMusicInfoTag()->GetDuration()); + } + write += StringUtils::Format(" </playlist>\n"); + write += StringUtils::Format("</WinampXML>\n"); + file.Write(write.c_str(), write.size()); + file.Close(); +} diff --git a/xbmc/playlists/PlayListB4S.h b/xbmc/playlists/PlayListB4S.h new file mode 100644 index 0000000..6f688b5 --- /dev/null +++ b/xbmc/playlists/PlayListB4S.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" + +namespace PLAYLIST +{ + +class CPlayListB4S : + public CPlayList +{ +public: + CPlayListB4S(void); + ~CPlayListB4S(void) override; + bool LoadData(std::istream& stream) override; + void Save(const std::string& strFileName) const override; +}; +} diff --git a/xbmc/playlists/PlayListFactory.cpp b/xbmc/playlists/PlayListFactory.cpp new file mode 100644 index 0000000..8abb1ba --- /dev/null +++ b/xbmc/playlists/PlayListFactory.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListFactory.h" + +#include "FileItem.h" +#include "playlists/PlayListB4S.h" +#include "playlists/PlayListM3U.h" +#include "playlists/PlayListPLS.h" +#include "playlists/PlayListURL.h" +#include "playlists/PlayListWPL.h" +#include "playlists/PlayListXML.h" +#include "playlists/PlayListXSPF.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace PLAYLIST; + +CPlayList* CPlayListFactory::Create(const std::string& filename) +{ + CFileItem item(filename,false); + return Create(item); +} + +CPlayList* CPlayListFactory::Create(const CFileItem& item) +{ + if (item.IsInternetStream()) + { + // Ensure the MIME type has been retrieved for http:// and shout:// streams + if (item.GetMimeType().empty()) + const_cast<CFileItem&>(item).FillInMimeType(); + + std::string strMimeType = item.GetMimeType(); + StringUtils::ToLower(strMimeType); + + if (strMimeType == "video/x-ms-asf" + || strMimeType == "video/x-ms-asx" + || strMimeType == "video/x-ms-wmv" + || strMimeType == "video/x-ms-wma" + || strMimeType == "video/x-ms-wfs" + || strMimeType == "video/x-ms-wvx" + || strMimeType == "video/x-ms-wax") + return new CPlayListASX(); + + if (strMimeType == "audio/x-pn-realaudio") + return new CPlayListRAM(); + + if (strMimeType == "audio/x-scpls" + || strMimeType == "playlist" + || strMimeType == "text/html") + return new CPlayListPLS(); + + // online m3u8 files are for hls streaming -- do not treat as playlist + if (strMimeType == "audio/x-mpegurl" && !item.IsType(".m3u8")) + return new CPlayListM3U(); + + if (strMimeType == "application/vnd.ms-wpl") + return new CPlayListWPL(); + + if (strMimeType == "application/xspf+xml") + return new CPlayListXSPF(); + } + + std::string path = item.GetDynPath(); + + std::string extension = URIUtils::GetExtension(path); + StringUtils::ToLower(extension); + + if (extension == ".m3u" || extension == ".strm") + return new CPlayListM3U(); + + if (extension == ".pls") + return new CPlayListPLS(); + + if (extension == ".b4s") + return new CPlayListB4S(); + + if (extension == ".wpl") + return new CPlayListWPL(); + + if (extension == ".asx") + return new CPlayListASX(); + + if (extension == ".ram") + return new CPlayListRAM(); + + if (extension == ".url") + return new CPlayListURL(); + + if (extension == ".pxml") + return new CPlayListXML(); + + if (extension == ".xspf") + return new CPlayListXSPF(); + + return NULL; + +} + +bool CPlayListFactory::IsPlaylist(const CFileItem& item) +{ + std::string strMimeType = item.GetMimeType(); + StringUtils::ToLower(strMimeType); + +/* These are a bit uncertain + if(strMimeType == "video/x-ms-asf" + || strMimeType == "video/x-ms-asx" + || strMimeType == "video/x-ms-wmv" + || strMimeType == "video/x-ms-wma" + || strMimeType == "video/x-ms-wfs" + || strMimeType == "video/x-ms-wvx" + || strMimeType == "video/x-ms-wax" + || strMimeType == "video/x-ms-asf") + return true; +*/ + + // online m3u8 files are hls:// -- do not treat as playlist + if (item.IsInternetStream() && item.IsType(".m3u8")) + return false; + + if(strMimeType == "audio/x-pn-realaudio" + || strMimeType == "playlist" + || strMimeType == "audio/x-mpegurl") + return true; + + return IsPlaylist(item.GetDynPath()); +} + +bool CPlayListFactory::IsPlaylist(const CURL& url) +{ + return URIUtils::HasExtension(url, + ".m3u|.b4s|.pls|.strm|.wpl|.asx|.ram|.url|.pxml|.xspf"); +} + +bool CPlayListFactory::IsPlaylist(const std::string& filename) +{ + return URIUtils::HasExtension(filename, + ".m3u|.b4s|.pls|.strm|.wpl|.asx|.ram|.url|.pxml|.xspf"); +} + diff --git a/xbmc/playlists/PlayListFactory.h b/xbmc/playlists/PlayListFactory.h new file mode 100644 index 0000000..59a56f2 --- /dev/null +++ b/xbmc/playlists/PlayListFactory.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <string> + +class CFileItem; +class CURL; + +namespace PLAYLIST +{ + class CPlayList; + + class CPlayListFactory + { + public: + static CPlayList* Create(const std::string& filename); + static CPlayList* Create(const CFileItem& item); + static bool IsPlaylist(const CURL& url); + static bool IsPlaylist(const std::string& filename); + static bool IsPlaylist(const CFileItem& item); + }; +} diff --git a/xbmc/playlists/PlayListM3U.cpp b/xbmc/playlists/PlayListM3U.cpp new file mode 100644 index 0000000..d39cdce --- /dev/null +++ b/xbmc/playlists/PlayListM3U.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListM3U.h" + +#include "FileItem.h" +#include "URL.h" +#include "Util.h" +#include "filesystem/File.h" +#include "music/tags/MusicInfoTag.h" +#include "utils/CharsetConverter.h" +#include "utils/URIUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" + +#include <inttypes.h> + +using namespace PLAYLIST; +using namespace XFILE; + +const char* CPlayListM3U::StartMarker = "#EXTCPlayListM3U::M3U"; +const char* CPlayListM3U::InfoMarker = "#EXTINF"; +const char* CPlayListM3U::ArtistMarker = "#EXTART"; +const char* CPlayListM3U::AlbumMarker = "#EXTALB"; +const char* CPlayListM3U::PropertyMarker = "#KODIPROP"; +const char* CPlayListM3U::VLCOptMarker = "#EXTVLCOPT"; +const char* CPlayListM3U::StreamMarker = "#EXT-X-STREAM-INF"; +const char* CPlayListM3U::BandwidthMarker = "BANDWIDTH"; +const char* CPlayListM3U::OffsetMarker = "#EXT-KX-OFFSET"; + +// example m3u file: +// #EXTM3U +// #EXTART:Demo Artist +// #EXTALB:Demo Album +// #KODIPROP:name=value +// #EXTINF:5,demo +// E:\Program Files\Winamp3\demo.mp3 + + + +// example m3u8 containing streams of different bitrates +// #EXTM3U +// #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1600000 +// playlist_1600.m3u8 +// #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=3000000 +// playlist_3000.m3u8 +// #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=800000 +// playlist_800.m3u8 + + +CPlayListM3U::CPlayListM3U(void) = default; + +CPlayListM3U::~CPlayListM3U(void) = default; + + +bool CPlayListM3U::Load(const std::string& strFileName) +{ + char szLine[4096]; + std::string strLine; + std::string strInfo; + std::vector<std::pair<std::string, std::string> > properties; + + int lDuration = 0; + int iStartOffset = 0; + int iEndOffset = 0; + + Clear(); + + m_strPlayListName = URIUtils::GetFileName(strFileName); + URIUtils::GetParentPath(strFileName, m_strBasePath); + + CFile file; + if (!file.Open(strFileName) ) + { + file.Close(); + return false; + } + + while (file.ReadString(szLine, 4095)) + { + strLine = szLine; + StringUtils::Trim(strLine); + + if (StringUtils::StartsWith(strLine, InfoMarker)) + { + // start of info + size_t iColon = strLine.find(':'); + size_t iComma = strLine.find(','); + if (iColon != std::string::npos && + iComma != std::string::npos && + iComma > iColon) + { + // Read the info and duration + iColon++; + std::string strLength = strLine.substr(iColon, iComma - iColon); + lDuration = atoi(strLength.c_str()); + iComma++; + strInfo = strLine.substr(iComma); + g_charsetConverter.unknownToUTF8(strInfo); + } + } + else if (StringUtils::StartsWith(strLine, OffsetMarker)) + { + size_t iColon = strLine.find(':'); + size_t iComma = strLine.find(','); + if (iColon != std::string::npos && + iComma != std::string::npos && + iComma > iColon) + { + // Read the start and end offset + iColon++; + iStartOffset = atoi(strLine.substr(iColon, iComma - iColon).c_str()); + iComma++; + iEndOffset = atoi(strLine.substr(iComma).c_str()); + } + } + else if (StringUtils::StartsWith(strLine, PropertyMarker) + || StringUtils::StartsWith(strLine, VLCOptMarker)) + { + size_t iColon = strLine.find(':'); + size_t iEqualSign = strLine.find('='); + if (iColon != std::string::npos && + iEqualSign != std::string::npos && + iEqualSign > iColon) + { + std::string strFirst, strSecond; + properties.emplace_back( + StringUtils::Trim((strFirst = strLine.substr(iColon + 1, iEqualSign - iColon - 1))), + StringUtils::Trim((strSecond = strLine.substr(iEqualSign + 1)))); + } + } + else if (strLine != StartMarker && + !StringUtils::StartsWith(strLine, ArtistMarker) && + !StringUtils::StartsWith(strLine, AlbumMarker)) + { + std::string strFileName = strLine; + + if (!strFileName.empty() && strFileName[0] == '#') + continue; // assume a comment or something else we don't support + + // Skip self - do not load playlist recursively + // We compare case-less in case user has input incorrect case of the current playlist + if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strFileName), m_strPlayListName)) + continue; + + if (strFileName.length() > 0) + { + g_charsetConverter.unknownToUTF8(strFileName); + + // If no info was read from from the extended tag information, use the file name + if (strInfo.length() == 0) + { + strInfo = URIUtils::GetFileName(strFileName); + } + + // should substitution occur before or after charset conversion?? + strFileName = URIUtils::SubstitutePath(strFileName); + + // Get the full path file name and add it to the the play list + CUtil::GetQualifiedFilename(m_strBasePath, strFileName); + CFileItemPtr newItem(new CFileItem(strInfo)); + newItem->SetPath(strFileName); + if (iStartOffset != 0 || iEndOffset != 0) + { + newItem->SetStartOffset(iStartOffset); + newItem->m_lStartPartNumber = 1; + newItem->SetProperty("item_start", iStartOffset); + newItem->SetEndOffset(iEndOffset); + // Prevent load message from file and override offset set here + newItem->GetMusicInfoTag()->SetLoaded(); + newItem->GetMusicInfoTag()->SetTitle(strInfo); + if (iEndOffset) + lDuration = static_cast<int>(CUtil::ConvertMilliSecsToSecsIntRounded(iEndOffset - iStartOffset)); + } + if (newItem->IsVideo() && !newItem->HasVideoInfoTag()) // File is a video and needs a VideoInfoTag + newItem->GetVideoInfoTag()->Reset(); // Force VideoInfoTag creation + if (lDuration && newItem->IsAudio()) + newItem->GetMusicInfoTag()->SetDuration(lDuration); + for (auto &prop : properties) + { + newItem->SetProperty(prop.first, prop.second); + } + + newItem->SetMimeType(newItem->GetProperty("mimetype").asString()); + if (!newItem->GetMimeType().empty()) + newItem->SetContentLookup(false); + + Add(newItem); + + // Reset the values just in case there part of the file have the extended marker + // and part don't + strInfo = ""; + lDuration = 0; + iStartOffset = 0; + iEndOffset = 0; + properties.clear(); + } + } + } + + file.Close(); + return true; +} + +void CPlayListM3U::Save(const std::string& strFileName) const +{ + if (!m_vecItems.size()) + return; + std::string strPlaylist = CUtil::MakeLegalPath(strFileName); + CFile file; + if (!file.OpenForWrite(strPlaylist,true)) + { + CLog::Log(LOGERROR, "Could not save M3U playlist: [{}]", strPlaylist); + return; + } + std::string strLine = StringUtils::Format("{}\n", StartMarker); + if (file.Write(strLine.c_str(), strLine.size()) != static_cast<ssize_t>(strLine.size())) + return; // error + + for (int i = 0; i < (int)m_vecItems.size(); ++i) + { + CFileItemPtr item = m_vecItems[i]; + std::string strDescription=item->GetLabel(); + g_charsetConverter.utf8ToStringCharset(strDescription); + strLine = StringUtils::Format("{}:{},{}\n", InfoMarker, + item->GetMusicInfoTag()->GetDuration(), strDescription); + if (file.Write(strLine.c_str(), strLine.size()) != static_cast<ssize_t>(strLine.size())) + return; // error + if (item->GetStartOffset() != 0 || item->GetEndOffset() != 0) + { + strLine = StringUtils::Format("{}:{},{}\n", OffsetMarker, item->GetStartOffset(), + item->GetEndOffset()); + file.Write(strLine.c_str(),strLine.size()); + } + std::string strFileName = ResolveURL(item); + g_charsetConverter.utf8ToStringCharset(strFileName); + strLine = StringUtils::Format("{}\n", strFileName); + if (file.Write(strLine.c_str(), strLine.size()) != static_cast<ssize_t>(strLine.size())) + return; // error + } + file.Close(); +} + +std::map< std::string, std::string > CPlayListM3U::ParseStreamLine(const std::string &streamLine) +{ + std::map< std::string, std::string > params; + + // ensure the line has something beyond the stream marker and ':' + if (streamLine.size() < strlen(StreamMarker) + 2) + return params; + + // get the actual params following the : + std::string strParams(streamLine.substr(strlen(StreamMarker) + 1)); + + // separate the parameters + std::vector<std::string> vecParams = StringUtils::Split(strParams, ","); + for (std::vector<std::string>::iterator i = vecParams.begin(); i != vecParams.end(); ++i) + { + // split the param, ensure there was an = + StringUtils::Trim(*i); + std::vector<std::string> vecTuple = StringUtils::Split(*i, "="); + if (vecTuple.size() < 2) + continue; + + // remove white space from name and value and store it in the dictionary + StringUtils::Trim(vecTuple[0]); + StringUtils::Trim(vecTuple[1]); + params[vecTuple[0]] = vecTuple[1]; + } + + return params; +} + diff --git a/xbmc/playlists/PlayListM3U.h b/xbmc/playlists/PlayListM3U.h new file mode 100644 index 0000000..467ed30 --- /dev/null +++ b/xbmc/playlists/PlayListM3U.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" +#include "URL.h" + +namespace PLAYLIST +{ +class CPlayListM3U : + public CPlayList +{ +public: + static const char *StartMarker; + static const char *InfoMarker; + static const char *ArtistMarker; + static const char *AlbumMarker; + static const char *PropertyMarker; + static const char *VLCOptMarker; + static const char *StreamMarker; + static const char *BandwidthMarker; + static const char *OffsetMarker; + +public: + CPlayListM3U(void); + ~CPlayListM3U(void) override; + bool Load(const std::string& strFileName) override; + void Save(const std::string& strFileName) const override; + + static std::map<std::string,std::string> ParseStreamLine(const std::string &streamLine); +}; +} diff --git a/xbmc/playlists/PlayListPLS.cpp b/xbmc/playlists/PlayListPLS.cpp new file mode 100644 index 0000000..369804d --- /dev/null +++ b/xbmc/playlists/PlayListPLS.cpp @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListPLS.h" + +#include "FileItem.h" +#include "PlayListFactory.h" +#include "Util.h" +#include "filesystem/File.h" +#include "music/tags/MusicInfoTag.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" +#include "video/VideoInfoTag.h" + +#include <iostream> +#include <memory> +#include <string> +#include <vector> + +using namespace XFILE; +using namespace PLAYLIST; + +#define START_PLAYLIST_MARKER "[playlist]" // may be case-insensitive (equivalent to .ini file on win32) +#define PLAYLIST_NAME "PlaylistName" + +/*---------------------------------------------------------------------- +[playlist] +PlaylistName=Playlist 001 +File1=E:\Program Files\Winamp3\demo.mp3 +Title1=demo +Length1=5 +File2=E:\Program Files\Winamp3\demo.mp3 +Title2=demo +Length2=5 +NumberOfEntries=2 +Version=2 +----------------------------------------------------------------------*/ +CPlayListPLS::CPlayListPLS(void) = default; + +CPlayListPLS::~CPlayListPLS(void) = default; + +bool CPlayListPLS::Load(const std::string &strFile) +{ + //read it from the file + std::string strFileName(strFile); + m_strPlayListName = URIUtils::GetFileName(strFileName); + + Clear(); + + bool bShoutCast = false; + if( StringUtils::StartsWithNoCase(strFileName, "shout://") ) + { + strFileName.replace(0, 8, "http://"); + m_strBasePath = ""; + bShoutCast = true; + } + else + URIUtils::GetParentPath(strFileName, m_strBasePath); + + CFile file; + if (!file.Open(strFileName) ) + { + file.Close(); + return false; + } + + if (file.GetLength() > 1024*1024) + { + CLog::Log(LOGWARNING, "{} - File is larger than 1 MB, most likely not a playlist", + __FUNCTION__); + return false; + } + + char szLine[4096]; + std::string strLine; + + // run through looking for the [playlist] marker. + // if we find another http stream, then load it. + while (true) + { + if ( !file.ReadString(szLine, sizeof(szLine) ) ) + { + file.Close(); + return size() > 0; + } + strLine = szLine; + StringUtils::Trim(strLine); + if(StringUtils::EqualsNoCase(strLine, START_PLAYLIST_MARKER)) + break; + + // if there is something else before playlist marker, this isn't a pls file + if(!strLine.empty()) + return false; + } + + bool bFailed = false; + while (file.ReadString(szLine, sizeof(szLine) ) ) + { + strLine = szLine; + StringUtils::RemoveCRLF(strLine); + size_t iPosEqual = strLine.find('='); + if (iPosEqual != std::string::npos) + { + std::string strLeft = strLine.substr(0, iPosEqual); + iPosEqual++; + std::string strValue = strLine.substr(iPosEqual); + StringUtils::ToLower(strLeft); + StringUtils::TrimLeft(strLeft); + + if (strLeft == "numberofentries") + { + m_vecItems.reserve(atoi(strValue.c_str())); + } + else if (StringUtils::StartsWith(strLeft, "file")) + { + std::vector <int>::size_type idx = atoi(strLeft.c_str() + 4); + if (!Resize(idx)) + { + bFailed = true; + break; + } + + // Skip self - do not load playlist recursively + if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strValue), + URIUtils::GetFileName(strFileName))) + continue; + + if (m_vecItems[idx - 1]->GetLabel().empty()) + m_vecItems[idx - 1]->SetLabel(URIUtils::GetFileName(strValue)); + CFileItem item(strValue, false); + if (bShoutCast && !item.IsAudio()) + strValue.replace(0, 7, "shout://"); + + strValue = URIUtils::SubstitutePath(strValue); + CUtil::GetQualifiedFilename(m_strBasePath, strValue); + g_charsetConverter.unknownToUTF8(strValue); + m_vecItems[idx - 1]->SetPath(strValue); + } + else if (StringUtils::StartsWith(strLeft, "title")) + { + std::vector <int>::size_type idx = atoi(strLeft.c_str() + 5); + if (!Resize(idx)) + { + bFailed = true; + break; + } + g_charsetConverter.unknownToUTF8(strValue); + m_vecItems[idx - 1]->SetLabel(strValue); + } + else if (StringUtils::StartsWith(strLeft, "length")) + { + std::vector <int>::size_type idx = atoi(strLeft.c_str() + 6); + if (!Resize(idx)) + { + bFailed = true; + break; + } + m_vecItems[idx - 1]->GetMusicInfoTag()->SetDuration(atol(strValue.c_str())); + } + else if (strLeft == "playlistname") + { + m_strPlayListName = strValue; + g_charsetConverter.unknownToUTF8(m_strPlayListName); + } + } + } + file.Close(); + + if (bFailed) + { + CLog::Log(LOGERROR, + "File {} is not a valid PLS playlist. Location of first file,title or length is not " + "permitted (eg. File0 should be File1)", + URIUtils::GetFileName(strFileName)); + return false; + } + + // check for missing entries + ivecItems p = m_vecItems.begin(); + while ( p != m_vecItems.end()) + { + if ((*p)->GetPath().empty()) + { + p = m_vecItems.erase(p); + } + else + { + ++p; + } + } + + return true; +} + +void CPlayListPLS::Save(const std::string& strFileName) const +{ + if (!m_vecItems.size()) return ; + std::string strPlaylist = CUtil::MakeLegalPath(strFileName); + CFile file; + if (!file.OpenForWrite(strPlaylist, true)) + { + CLog::Log(LOGERROR, "Could not save PLS playlist: [{}]", strPlaylist); + return; + } + std::string write; + write += StringUtils::Format("{}\n", START_PLAYLIST_MARKER); + std::string strPlayListName=m_strPlayListName; + g_charsetConverter.utf8ToStringCharset(strPlayListName); + write += StringUtils::Format("PlaylistName={}\n", strPlayListName); + + for (int i = 0; i < (int)m_vecItems.size(); ++i) + { + CFileItemPtr item = m_vecItems[i]; + std::string strFileName=item->GetPath(); + g_charsetConverter.utf8ToStringCharset(strFileName); + std::string strDescription=item->GetLabel(); + g_charsetConverter.utf8ToStringCharset(strDescription); + write += StringUtils::Format("File{}={}\n", i + 1, strFileName); + write += StringUtils::Format("Title{}={}\n", i + 1, strDescription.c_str()); + write += + StringUtils::Format("Length{}={}\n", i + 1, item->GetMusicInfoTag()->GetDuration() / 1000); + } + + write += StringUtils::Format("NumberOfEntries={0}\n", m_vecItems.size()); + write += StringUtils::Format("Version=2\n"); + file.Write(write.c_str(), write.size()); + file.Close(); +} + +bool CPlayListASX::LoadAsxIniInfo(std::istream &stream) +{ + CLog::Log(LOGINFO, "Parsing INI style ASX"); + + std::string name, value; + + while( stream.good() ) + { + // consume blank rows, and blanks + while((stream.peek() == '\r' || stream.peek() == '\n' || stream.peek() == ' ') && stream.good()) + stream.get(); + + if(stream.peek() == '[') + { + // this is an [section] part, just ignore it + while(stream.good() && stream.peek() != '\r' && stream.peek() != '\n') + stream.get(); + continue; + } + name = ""; + value = ""; + // consume name + while(stream.peek() != '\r' && stream.peek() != '\n' && stream.peek() != '=' && stream.good()) + name += stream.get(); + + // consume = + if(stream.get() != '=') + continue; + + // consume value + while(stream.peek() != '\r' && stream.peek() != '\n' && stream.good()) + value += stream.get(); + + CLog::Log(LOGINFO, "Adding element {}={}", name, value); + CFileItemPtr newItem(new CFileItem(value)); + newItem->SetPath(value); + if (newItem->IsVideo() && !newItem->HasVideoInfoTag()) // File is a video and needs a VideoInfoTag + newItem->GetVideoInfoTag()->Reset(); // Force VideoInfoTag creation + Add(newItem); + } + + return true; +} + +bool CPlayListASX::LoadData(std::istream& stream) +{ + CLog::Log(LOGINFO, "Parsing ASX"); + + if(stream.peek() == '[') + { + return LoadAsxIniInfo(stream); + } + else + { + std::string asxstream(std::istreambuf_iterator<char>(stream), {}); + CXBMCTinyXML xmlDoc; + xmlDoc.Parse(asxstream, TIXML_DEFAULT_ENCODING); + + if (xmlDoc.Error()) + { + CLog::Log(LOGERROR, "Unable to parse ASX info Error: {}", xmlDoc.ErrorDesc()); + return false; + } + + TiXmlElement *pRootElement = xmlDoc.RootElement(); + + if (!pRootElement) + return false; + + // lowercase every element + TiXmlNode *pNode = pRootElement; + TiXmlNode *pChild = NULL; + std::string value; + value = pNode->Value(); + StringUtils::ToLower(value); + pNode->SetValue(value); + while(pNode) + { + pChild = pNode->IterateChildren(pChild); + if(pChild) + { + if (pChild->Type() == TiXmlNode::TINYXML_ELEMENT) + { + value = pChild->Value(); + StringUtils::ToLower(value); + pChild->SetValue(value); + + TiXmlAttribute* pAttr = pChild->ToElement()->FirstAttribute(); + while(pAttr) + { + value = pAttr->Name(); + StringUtils::ToLower(value); + pAttr->SetName(value); + pAttr = pAttr->Next(); + } + } + + pNode = pChild; + pChild = NULL; + continue; + } + + pChild = pNode; + pNode = pNode->Parent(); + } + std::string roottitle; + TiXmlElement *pElement = pRootElement->FirstChildElement(); + while (pElement) + { + value = pElement->Value(); + if (value == "title" && !pElement->NoChildren()) + { + roottitle = pElement->FirstChild()->ValueStr(); + } + else if (value == "entry") + { + std::string title(roottitle); + + TiXmlElement *pRef = pElement->FirstChildElement("ref"); + TiXmlElement *pTitle = pElement->FirstChildElement("title"); + + if(pTitle && !pTitle->NoChildren()) + title = pTitle->FirstChild()->ValueStr(); + + while (pRef) + { // multiple references may appear for one entry + // duration may exist on this level too + value = XMLUtils::GetAttribute(pRef, "href"); + if (!value.empty()) + { + if(title.empty()) + title = value; + + CLog::Log(LOGINFO, "Adding element {}, {}", title, value); + CFileItemPtr newItem(new CFileItem(title)); + newItem->SetPath(value); + Add(newItem); + } + pRef = pRef->NextSiblingElement("ref"); + } + } + else if (value == "entryref") + { + value = XMLUtils::GetAttribute(pElement, "href"); + if (!value.empty()) + { // found an entryref, let's try loading that url + std::unique_ptr<CPlayList> playlist(CPlayListFactory::Create(value)); + if (nullptr != playlist) + if (playlist->Load(value)) + Add(*playlist); + } + } + pElement = pElement->NextSiblingElement(); + } + } + + return true; +} + + +bool CPlayListRAM::LoadData(std::istream& stream) +{ + CLog::Log(LOGINFO, "Parsing RAM"); + + std::string strMMS; + while( stream.peek() != '\n' && stream.peek() != '\r' ) + strMMS += stream.get(); + + CLog::Log(LOGINFO, "Adding element {}", strMMS); + CFileItemPtr newItem(new CFileItem(strMMS)); + newItem->SetPath(strMMS); + Add(newItem); + return true; +} + +bool CPlayListPLS::Resize(std::vector <int>::size_type newSize) +{ + if (newSize == 0) + return false; + + while (m_vecItems.size() < newSize) + { + CFileItemPtr fileItem(new CFileItem()); + m_vecItems.push_back(fileItem); + } + return true; +} diff --git a/xbmc/playlists/PlayListPLS.h b/xbmc/playlists/PlayListPLS.h new file mode 100644 index 0000000..50d58e3 --- /dev/null +++ b/xbmc/playlists/PlayListPLS.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" + +#include <string> +#include <vector> + +namespace PLAYLIST +{ +class CPlayListPLS : + public CPlayList +{ +public: + CPlayListPLS(void); + ~CPlayListPLS(void) override; + bool Load(const std::string& strFileName) override; + void Save(const std::string& strFileName) const override; + virtual bool Resize(std::vector<int>::size_type newSize); +}; + +class CPlayListASX : public CPlayList +{ +public: + bool LoadData(std::istream &stream) override; +protected: + bool LoadAsxIniInfo(std::istream &stream); +}; + +class CPlayListRAM : public CPlayList +{ +public: + bool LoadData(std::istream &stream) override; +}; + + +} diff --git a/xbmc/playlists/PlayListTypes.h b/xbmc/playlists/PlayListTypes.h new file mode 100644 index 0000000..84462c1 --- /dev/null +++ b/xbmc/playlists/PlayListTypes.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +namespace PLAYLIST +{ + +using Id = int; + +constexpr Id TYPE_NONE = -1; //! Playlist id of type none +constexpr Id TYPE_MUSIC = 0; //! Playlist id of type music +constexpr Id TYPE_VIDEO = 1; //! Playlist id of type video +constexpr Id TYPE_PICTURE = 2; //! Playlist id of type picture + +/*! + * \brief Manages playlist playing. + */ +enum class RepeatState +{ + NONE, + ONE, + ALL +}; + +} // namespace PLAYLIST diff --git a/xbmc/playlists/PlayListURL.cpp b/xbmc/playlists/PlayListURL.cpp new file mode 100644 index 0000000..f2ec4e3 --- /dev/null +++ b/xbmc/playlists/PlayListURL.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListURL.h" + +#include "FileItem.h" +#include "filesystem/File.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" + +using namespace PLAYLIST; +using namespace XFILE; + +// example url file +//[DEFAULT] +//BASEURL=http://msdn2.microsoft.com/en-us/library/ms812698.aspx +//[InternetShortcut] +//URL=http://msdn2.microsoft.com/en-us/library/ms812698.aspx + +CPlayListURL::CPlayListURL(void) = default; + +CPlayListURL::~CPlayListURL(void) = default; + +bool CPlayListURL::Load(const std::string& strFileName) +{ + char szLine[4096]; + std::string strLine; + + Clear(); + + m_strPlayListName = URIUtils::GetFileName(strFileName); + URIUtils::GetParentPath(strFileName, m_strBasePath); + + CFile file; + if (!file.Open(strFileName) ) + { + file.Close(); + return false; + } + + while (file.ReadString(szLine, 1024)) + { + strLine = szLine; + StringUtils::RemoveCRLF(strLine); + + if (StringUtils::StartsWith(strLine, "[InternetShortcut]")) + { + if (file.ReadString(szLine,1024)) + { + strLine = szLine; + StringUtils::RemoveCRLF(strLine); + if (StringUtils::StartsWith(strLine, "URL=")) + { + CFileItemPtr newItem(new CFileItem(strLine.substr(4), false)); + Add(newItem); + } + } + } + } + + file.Close(); + return true; +} + diff --git a/xbmc/playlists/PlayListURL.h b/xbmc/playlists/PlayListURL.h new file mode 100644 index 0000000..d0baa0a --- /dev/null +++ b/xbmc/playlists/PlayListURL.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" + +namespace PLAYLIST +{ +class CPlayListURL : + public CPlayList +{ +public: + CPlayListURL(void); + ~CPlayListURL(void) override; + bool Load(const std::string& strFileName) override; +}; +} diff --git a/xbmc/playlists/PlayListWPL.cpp b/xbmc/playlists/PlayListWPL.cpp new file mode 100644 index 0000000..e7142ff --- /dev/null +++ b/xbmc/playlists/PlayListWPL.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListWPL.h" + +#include "FileItem.h" +#include "Util.h" +#include "filesystem/File.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <iostream> +#include <string> + +using namespace XFILE; +using namespace PLAYLIST; + +/* ------------------------ example wpl playlist file --------------------------------- + <?wpl version="1.0"?> + <smil> + <head> + <meta name="Generator" content="Microsoft Windows Media Player -- 10.0.0.3646"/> + <author/> + <title>Playlist</title> + </head> + <body> + <seq> + <media src="E:\MP3\Track1.mp3"/> + <media src="E:\MP3\Track2.mp3"/> + <media src="E:\MP3\track3.mp3"/> + </seq> + </body> + </smil> +------------------------ end of example wpl playlist file ---------------------------------*/ +//Note: File is utf-8 encoded by default + +CPlayListWPL::CPlayListWPL(void) = default; + +CPlayListWPL::~CPlayListWPL(void) = default; + + +bool CPlayListWPL::LoadData(std::istream& stream) +{ + CXBMCTinyXML xmlDoc; + + stream >> xmlDoc; + if (xmlDoc.Error()) + { + CLog::Log(LOGERROR, "Unable to parse B4S info Error: {}", xmlDoc.ErrorDesc()); + return false; + } + + TiXmlElement* pRootElement = xmlDoc.RootElement(); + if (!pRootElement ) return false; + + TiXmlElement* pHeadElement = pRootElement->FirstChildElement("head"); + if (pHeadElement ) + { + TiXmlElement* pTitelElement = pHeadElement->FirstChildElement("title"); + if (pTitelElement ) + m_strPlayListName = pTitelElement->Value(); + } + + TiXmlElement* pBodyElement = pRootElement->FirstChildElement("body"); + if (!pBodyElement ) return false; + + TiXmlElement* pSeqElement = pBodyElement->FirstChildElement("seq"); + if (!pSeqElement ) return false; + + TiXmlElement* pMediaElement = pSeqElement->FirstChildElement("media"); + + if (!pMediaElement) return false; + while (pMediaElement) + { + std::string strFileName = XMLUtils::GetAttribute(pMediaElement, "src"); + if (!strFileName.empty()) + { + std::string strFileNameClean = URIUtils::SubstitutePath(strFileName); + CUtil::GetQualifiedFilename(m_strBasePath, strFileNameClean); + std::string strDescription = URIUtils::GetFileName(strFileNameClean); + CFileItemPtr newItem(new CFileItem(strDescription)); + newItem->SetPath(strFileNameClean); + Add(newItem); + } + pMediaElement = pMediaElement->NextSiblingElement(); + } + return true; +} + +void CPlayListWPL::Save(const std::string& strFileName) const +{ + if (!m_vecItems.size()) return ; + std::string strPlaylist = CUtil::MakeLegalPath(strFileName); + CFile file; + if (!file.OpenForWrite(strPlaylist, true)) + { + CLog::Log(LOGERROR, "Could not save WPL playlist: [{}]", strPlaylist); + return ; + } + std::string write; + write += StringUtils::Format("<?wpl version={}1.0{}>\n", 34, 34); + write += StringUtils::Format("<smil>\n"); + write += StringUtils::Format(" <head>\n"); + write += StringUtils::Format(" <meta name={}Generator{} content={}Microsoft Windows Media " + "Player -- 10.0.0.3646{}/>\n", + 34, 34, 34, 34); + write += StringUtils::Format(" <author/>\n"); + write += StringUtils::Format(" <title>{}</title>\n", m_strPlayListName.c_str()); + write += StringUtils::Format(" </head>\n"); + write += StringUtils::Format(" <body>\n"); + write += StringUtils::Format(" <seq>\n"); + for (int i = 0; i < (int)m_vecItems.size(); ++i) + { + CFileItemPtr item = m_vecItems[i]; + write += StringUtils::Format(" <media src={}{}{}/>", 34, item->GetPath(), 34); + } + write += StringUtils::Format(" </seq>\n"); + write += StringUtils::Format(" </body>\n"); + write += StringUtils::Format("</smil>\n"); + file.Write(write.c_str(), write.size()); + file.Close(); +} diff --git a/xbmc/playlists/PlayListWPL.h b/xbmc/playlists/PlayListWPL.h new file mode 100644 index 0000000..e0bdf68 --- /dev/null +++ b/xbmc/playlists/PlayListWPL.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" + +namespace PLAYLIST +{ + +class CPlayListWPL : + public CPlayList +{ +public: + CPlayListWPL(void); + ~CPlayListWPL(void) override; + bool LoadData(std::istream& stream) override; + void Save(const std::string& strFileName) const override; +}; +} diff --git a/xbmc/playlists/PlayListXML.cpp b/xbmc/playlists/PlayListXML.cpp new file mode 100644 index 0000000..82af079 --- /dev/null +++ b/xbmc/playlists/PlayListXML.cpp @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListXML.h" + +#include "FileItem.h" +#include "Util.h" +#include "filesystem/File.h" +#include "media/MediaLockState.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +using namespace PLAYLIST; +using namespace XFILE; + +/* + Playlist example (must be stored with .pxml extension): + +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<streams> + <!-- Stream definition. To have multiple streams, just add another <stream>...</stream> set. !--> + <stream> + <!-- Stream URL !--> + <url>mms://stream02.rambler.ru/eurosport</url> + <!-- Stream name - used for display !--> + <name>Евроспорт</name> + <!-- Stream category - currently only LIVETV is supported !--> + <category>LIVETV</category> + <!-- Stream language code !--> + <lang>RU</lang> + <!-- Stream channel number - will be used to select stream by channel number !--> + <channel>1</channel> + <!-- Stream is password-protected !--> + <lockpassword>123</lockpassword> + </stream> + + <stream> + <url>mms://video.rfn.ru/vesti_24</url> + <name>Вести 24</name> + <category>LIVETV</category> + <lang>RU</lang> + <channel>2</channel> + </stream> + +</streams> +*/ + + +CPlayListXML::CPlayListXML(void) = default; + +CPlayListXML::~CPlayListXML(void) = default; + + +static inline std::string GetString( const TiXmlElement* pRootElement, const char *tagName ) +{ + std::string strValue; + if ( XMLUtils::GetString(pRootElement, tagName, strValue) ) + return strValue; + + return ""; +} + +bool CPlayListXML::Load( const std::string& strFileName ) +{ + CXBMCTinyXML xmlDoc; + + m_strPlayListName = URIUtils::GetFileName(strFileName); + URIUtils::GetParentPath(strFileName, m_strBasePath); + + Clear(); + + // Try to load the file as XML. If it does not load, return an error. + if ( !xmlDoc.LoadFile( strFileName ) ) + { + CLog::Log(LOGERROR, "Playlist {} has invalid format/malformed xml", strFileName); + return false; + } + + TiXmlElement *pRootElement = xmlDoc.RootElement(); + + // If the stream does not contain "streams", still ok. Not an error. + if (!pRootElement || StringUtils::CompareNoCase(pRootElement->Value(), "streams")) + { + CLog::Log(LOGERROR, "Playlist {} has no <streams> root", strFileName); + return false; + } + + TiXmlElement* pSet = pRootElement->FirstChildElement("stream"); + + while ( pSet ) + { + // Get parameters + std::string url = GetString( pSet, "url" ); + std::string name = GetString( pSet, "name" ); + std::string category = GetString( pSet, "category" ); + std::string lang = GetString( pSet, "lang" ); + std::string channel = GetString( pSet, "channel" ); + std::string lockpass = GetString( pSet, "lockpassword" ); + + // If url is empty, it doesn't make any sense + if ( !url.empty() ) + { + // If the name is empty, use url + if ( name.empty() ) + name = url; + + // Append language to the name, and also set as metadata + if ( !lang.empty() ) + name += " [" + lang + "]"; + + std::string info = name; + CFileItemPtr newItem( new CFileItem(info) ); + newItem->SetPath(url); + + // Set language as metadata + if ( !lang.empty() ) + newItem->SetProperty("language", lang.c_str() ); + + // Set category as metadata + if ( !category.empty() ) + newItem->SetProperty("category", category.c_str() ); + + // Set channel as extra info and as metadata + if ( !channel.empty() ) + { + newItem->SetProperty("remotechannel", channel.c_str() ); + newItem->SetExtraInfo( "Channel: " + channel ); + } + + if ( !lockpass.empty() ) + { + newItem->m_strLockCode = lockpass; + newItem->m_iHasLock = LOCK_STATE_LOCKED; + newItem->m_iLockMode = LOCK_MODE_NUMERIC; + } + + Add(newItem); + } + else + CLog::Log(LOGERROR, "Playlist entry {} in file {} has missing <url> tag", name, strFileName); + + pSet = pSet->NextSiblingElement("stream"); + } + + return true; +} + + +void CPlayListXML::Save(const std::string& strFileName) const +{ + if (!m_vecItems.size()) return ; + std::string strPlaylist = CUtil::MakeLegalPath(strFileName); + CFile file; + if (!file.OpenForWrite(strPlaylist, true)) + { + CLog::Log(LOGERROR, "Could not save WPL playlist: [{}]", strPlaylist); + return ; + } + std::string write; + write += StringUtils::Format("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"); + write += StringUtils::Format("<streams>\n"); + for (int i = 0; i < (int)m_vecItems.size(); ++i) + { + CFileItemPtr item = m_vecItems[i]; + write += StringUtils::Format(" <stream>\n" ); + write += StringUtils::Format(" <url>{}</url>", item->GetPath().c_str()); + write += StringUtils::Format(" <name>{}</name>", item->GetLabel()); + + if ( !item->GetProperty("language").empty() ) + write += StringUtils::Format(" <lang>{}</lang>", item->GetProperty("language").asString()); + + if ( !item->GetProperty("category").empty() ) + write += StringUtils::Format(" <category>{}</category>", + item->GetProperty("category").asString()); + + if ( !item->GetProperty("remotechannel").empty() ) + write += StringUtils::Format(" <channel>{}</channel>", + item->GetProperty("remotechannel").asString()); + + if (item->m_iHasLock > LOCK_STATE_NO_LOCK) + write += StringUtils::Format(" <lockpassword>{}<lockpassword>", item->m_strLockCode); + + write += StringUtils::Format(" </stream>\n\n" ); + } + + write += StringUtils::Format("</streams>\n"); + file.Write(write.c_str(), write.size()); + file.Close(); +} diff --git a/xbmc/playlists/PlayListXML.h b/xbmc/playlists/PlayListXML.h new file mode 100644 index 0000000..d1e18bc --- /dev/null +++ b/xbmc/playlists/PlayListXML.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" + +namespace PLAYLIST +{ +class CPlayListXML : + public CPlayList +{ +public: + CPlayListXML(void); + ~CPlayListXML(void) override; + bool Load(const std::string& strFileName) override; + void Save(const std::string& strFileName) const override; +}; +} diff --git a/xbmc/playlists/PlayListXSPF.cpp b/xbmc/playlists/PlayListXSPF.cpp new file mode 100644 index 0000000..6f665a7 --- /dev/null +++ b/xbmc/playlists/PlayListXSPF.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 Tyler Szabo + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PlayListXSPF.h" + +#include "FileItem.h" +#include "URL.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/log.h" + +using namespace PLAYLIST; + +namespace +{ + +constexpr char const* LOCATION_TAGNAME = "location"; +constexpr char const* PLAYLIST_TAGNAME = "playlist"; +constexpr char const* TITLE_TAGNAME = "title"; +constexpr char const* TRACK_TAGNAME = "track"; +constexpr char const* TRACKLIST_TAGNAME = "trackList"; + +std::string GetXMLText(const TiXmlElement* pXmlElement) +{ + std::string result; + if (pXmlElement) + { + const char* const innerText = pXmlElement->GetText(); + if (innerText) + result = innerText; + } + return result; +} + +} + +CPlayListXSPF::CPlayListXSPF(void) = default; + +CPlayListXSPF::~CPlayListXSPF(void) = default; + +bool CPlayListXSPF::Load(const std::string& strFileName) +{ + CXBMCTinyXML xmlDoc; + + if (!xmlDoc.LoadFile(strFileName)) + { + CLog::Log(LOGERROR, "Error parsing XML file {} ({}, {}): {}", strFileName, xmlDoc.ErrorRow(), + xmlDoc.ErrorCol(), xmlDoc.ErrorDesc()); + return false; + } + + TiXmlElement* pPlaylist = xmlDoc.FirstChildElement(PLAYLIST_TAGNAME); + if (!pPlaylist) + { + CLog::Log(LOGERROR, "Error parsing XML file {}: missing root element {}", strFileName, + PLAYLIST_TAGNAME); + return false; + } + + TiXmlElement* pTracklist = pPlaylist->FirstChildElement(TRACKLIST_TAGNAME); + if (!pTracklist) + { + CLog::Log(LOGERROR, "Error parsing XML file {}: missing element {}", strFileName, + TRACKLIST_TAGNAME); + return false; + } + + Clear(); + URIUtils::GetParentPath(strFileName, m_strBasePath); + + m_strPlayListName = GetXMLText(pPlaylist->FirstChildElement(TITLE_TAGNAME)); + + TiXmlElement* pCurTrack = pTracklist->FirstChildElement(TRACK_TAGNAME); + while (pCurTrack) + { + std::string location = GetXMLText(pCurTrack->FirstChildElement(LOCATION_TAGNAME)); + if (!location.empty()) + { + std::string label = GetXMLText(pCurTrack->FirstChildElement(TITLE_TAGNAME)); + + CFileItemPtr newItem(new CFileItem(label)); + + CURL uri(location); + + // at the time of writing CURL doesn't handle file:// URI scheme the way + // it's presented in this format, parse to local path instead + std::string localpath; + if (StringUtils::StartsWith(location, "file:///")) + { +#ifndef TARGET_WINDOWS + // Linux absolute path must start with root + localpath = "/"; +#endif + // Path starts after "file:///" + localpath += CURL::Decode(location.substr(8)); + } + else if (uri.GetProtocol().empty()) + { + localpath = URIUtils::AppendSlash(m_strBasePath) + CURL::Decode(location); + } + + if (!localpath.empty()) + { + // We don't use URIUtils::CanonicalizePath because m_strBasePath may be a + // protocol e.g. smb +#ifdef TARGET_WINDOWS + StringUtils::Replace(localpath, "/", "\\"); +#endif + localpath = URIUtils::GetRealPath(localpath); + + newItem->SetPath(localpath); + } + else + { + newItem->SetURL(uri); + } + + Add(newItem); + } + + pCurTrack = pCurTrack->NextSiblingElement(TRACK_TAGNAME); + } + + return true; +} diff --git a/xbmc/playlists/PlayListXSPF.h b/xbmc/playlists/PlayListXSPF.h new file mode 100644 index 0000000..8960460 --- /dev/null +++ b/xbmc/playlists/PlayListXSPF.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PlayList.h" + +namespace PLAYLIST +{ +class CPlayListXSPF : public CPlayList +{ +public: + CPlayListXSPF(void); + ~CPlayListXSPF(void) override; + + // Implementation of CPlayList + bool Load(const std::string& strFileName) override; +}; +} diff --git a/xbmc/playlists/SmartPlayList.cpp b/xbmc/playlists/SmartPlayList.cpp new file mode 100644 index 0000000..dc83a75 --- /dev/null +++ b/xbmc/playlists/SmartPlayList.cpp @@ -0,0 +1,1610 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SmartPlayList.h" + +#include "ServiceBroker.h" +#include "Util.h" +#include "dbwrappers/Database.h" +#include "filesystem/File.h" +#include "filesystem/SmartPlaylistDirectory.h" +#include "guilib/LocalizeStrings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/DatabaseUtils.h" +#include "utils/JSONVariantParser.h" +#include "utils/JSONVariantWriter.h" +#include "utils/StreamDetails.h" +#include "utils/StringUtils.h" +#include "utils/StringValidation.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <cstdlib> +#include <memory> +#include <set> +#include <string> +#include <vector> + +using namespace XFILE; + +typedef struct +{ + char string[17]; + Field field; + CDatabaseQueryRule::FIELD_TYPE type; + StringValidation::Validator validator; + bool browseable; + int localizedString; +} translateField; + +// clang-format off +static const translateField fields[] = { + { "none", FieldNone, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 231 }, + { "filename", FieldFilename, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 561 }, + { "path", FieldPath, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 573 }, + { "album", FieldAlbum, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 558 }, + { "albumartist", FieldAlbumArtist, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 566 }, + { "artist", FieldArtist, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 557 }, + { "tracknumber", FieldTrackNumber, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 554 }, + { "role", FieldRole, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 38033 }, + { "comment", FieldComment, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 569 }, + { "review", FieldReview, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 183 }, + { "themes", FieldThemes, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21895 }, + { "moods", FieldMoods, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 175 }, + { "styles", FieldStyles, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 176 }, + { "type", FieldAlbumType, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 564 }, + { "compilation", FieldCompilation, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 204 }, + { "label", FieldMusicLabel, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21899 }, + { "title", FieldTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 556 }, + { "sorttitle", FieldSortTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 171 }, + { "originaltitle", FieldOriginalTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 20376 }, + { "year", FieldYear, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, true, 562 }, + { "time", FieldTime, CDatabaseQueryRule::SECONDS_FIELD, StringValidation::IsTime, false, 180 }, + { "playcount", FieldPlaycount, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 567 }, + { "lastplayed", FieldLastPlayed, CDatabaseQueryRule::DATE_FIELD, NULL, false, 568 }, + { "inprogress", FieldInProgress, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 575 }, + { "rating", FieldRating, CDatabaseQueryRule::REAL_FIELD, CSmartPlaylistRule::ValidateRating, false, 563 }, + { "userrating", FieldUserRating, CDatabaseQueryRule::REAL_FIELD, CSmartPlaylistRule::ValidateMyRating, false, 38018 }, + { "votes", FieldVotes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 205 }, + { "top250", FieldTop250, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 13409 }, + { "mpaarating", FieldMPAA, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 20074 }, + { "dateadded", FieldDateAdded, CDatabaseQueryRule::DATE_FIELD, NULL, false, 570 }, + { "datemodified", FieldDateModified, CDatabaseQueryRule::DATE_FIELD, NULL, false, 39119 }, + { "datenew", FieldDateNew, CDatabaseQueryRule::DATE_FIELD, NULL, false, 21877 }, + { "genre", FieldGenre, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 515 }, + { "plot", FieldPlot, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 207 }, + { "plotoutline", FieldPlotOutline, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 203 }, + { "tagline", FieldTagline, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 202 }, + { "set", FieldSet, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20457 }, + { "director", FieldDirector, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20339 }, + { "actor", FieldActor, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20337 }, + { "writers", FieldWriter, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20417 }, + { "airdate", FieldAirDate, CDatabaseQueryRule::DATE_FIELD, NULL, false, 20416 }, + { "hastrailer", FieldTrailer, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 20423 }, + { "studio", FieldStudio, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 572 }, + { "country", FieldCountry, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 574 }, + { "tvshow", FieldTvShowTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20364 }, + { "status", FieldTvShowStatus, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 126 }, + { "season", FieldSeason, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 20373 }, + { "episode", FieldEpisodeNumber, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 20359 }, + { "numepisodes", FieldNumberOfEpisodes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 20360 }, + { "numwatched", FieldNumberOfWatchedEpisodes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21457 }, + { "videoresolution", FieldVideoResolution, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21443 }, + { "videocodec", FieldVideoCodec, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21445 }, + { "videoaspect", FieldVideoAspectRatio, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21374 }, + { "audiochannels", FieldAudioChannels, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21444 }, + { "audiocodec", FieldAudioCodec, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21446 }, + { "audiolanguage", FieldAudioLanguage, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21447 }, + { "audiocount", FieldAudioCount, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21481 }, + { "subtitlecount", FieldSubtitleCount, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21482 }, + { "subtitlelanguage", FieldSubtitleLanguage, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21448 }, + { "random", FieldRandom, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 590 }, + { "playlist", FieldPlaylist, CDatabaseQueryRule::PLAYLIST_FIELD, NULL, true, 559 }, + { "virtualfolder", FieldVirtualFolder, CDatabaseQueryRule::PLAYLIST_FIELD, NULL, true, 614 }, + { "tag", FieldTag, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20459 }, + { "instruments", FieldInstruments, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21892 }, + { "biography", FieldBiography, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21887 }, + { "born", FieldBorn, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21893 }, + { "bandformed", FieldBandFormed, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21894 }, + { "disbanded", FieldDisbanded, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21896 }, + { "died", FieldDied, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21897 }, + { "artisttype", FieldArtistType, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 564 }, + { "gender", FieldGender, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 39025 }, + { "disambiguation", FieldDisambiguation, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 39026 }, + { "source", FieldSource, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 39030 }, + { "disctitle", FieldDiscTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 38076 }, + { "isboxset", FieldIsBoxset, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 38074 }, + { "totaldiscs", FieldTotalDiscs, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 38077 }, + { "originalyear", FieldOrigYear, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, true, 38078 }, + { "bpm", FieldBPM, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 38080 }, + { "samplerate", FieldSampleRate, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 613 }, + { "bitrate", FieldMusicBitRate, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 623 }, + { "channels", FieldNoOfChannels, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 253 }, + { "albumstatus", FieldAlbumStatus, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 38081 }, + { "albumduration", FieldAlbumDuration, CDatabaseQueryRule::SECONDS_FIELD, StringValidation::IsTime, false, 180 }, + { "hdrtype", FieldHdrType, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 20474 }, +}; +// clang-format on + +typedef struct +{ + std::string name; + Field field; + bool canMix; + int localizedString; +} group; + +// clang-format off +static const group groups[] = { { "", FieldUnknown, false, 571 }, + { "none", FieldNone, false, 231 }, + { "sets", FieldSet, true, 20434 }, + { "genres", FieldGenre, false, 135 }, + { "years", FieldYear, false, 652 }, + { "actors", FieldActor, false, 344 }, + { "directors", FieldDirector, false, 20348 }, + { "writers", FieldWriter, false, 20418 }, + { "studios", FieldStudio, false, 20388 }, + { "countries", FieldCountry, false, 20451 }, + { "artists", FieldArtist, false, 133 }, + { "albums", FieldAlbum, false, 132 }, + { "tags", FieldTag, false, 20459 }, + { "originalyears", FieldOrigYear, false, 38078 }, + }; +// clang-format on + +#define RULE_VALUE_SEPARATOR " / " + +CSmartPlaylistRule::CSmartPlaylistRule() = default; + +int CSmartPlaylistRule::TranslateField(const char *field) const +{ + for (const translateField& f : fields) + if (StringUtils::EqualsNoCase(field, f.string)) return f.field; + return FieldNone; +} + +std::string CSmartPlaylistRule::TranslateField(int field) const +{ + for (const translateField& f : fields) + if (field == f.field) return f.string; + return "none"; +} + +SortBy CSmartPlaylistRule::TranslateOrder(const char *order) +{ + return SortUtils::SortMethodFromString(order); +} + +std::string CSmartPlaylistRule::TranslateOrder(SortBy order) +{ + std::string sortOrder = SortUtils::SortMethodToString(order); + if (sortOrder.empty()) + return "none"; + + return sortOrder; +} + +Field CSmartPlaylistRule::TranslateGroup(const char *group) +{ + for (const auto & i : groups) + { + if (StringUtils::EqualsNoCase(group, i.name)) + return i.field; + } + + return FieldUnknown; +} + +std::string CSmartPlaylistRule::TranslateGroup(Field group) +{ + for (const auto & i : groups) + { + if (group == i.field) + return i.name; + } + + return ""; +} + +std::string CSmartPlaylistRule::GetLocalizedField(int field) +{ + for (const translateField& f : fields) + if (field == f.field) return g_localizeStrings.Get(f.localizedString); + return g_localizeStrings.Get(16018); +} + +CDatabaseQueryRule::FIELD_TYPE CSmartPlaylistRule::GetFieldType(int field) const +{ + for (const translateField& f : fields) + if (field == f.field) return f.type; + return TEXT_FIELD; +} + +bool CSmartPlaylistRule::IsFieldBrowseable(int field) +{ + for (const translateField& f : fields) + if (field == f.field) return f.browseable; + + return false; +} + +bool CSmartPlaylistRule::Validate(const std::string &input, void *data) +{ + if (data == NULL) + return true; + + CSmartPlaylistRule *rule = static_cast<CSmartPlaylistRule*>(data); + + // check if there's a validator for this rule + StringValidation::Validator validator = NULL; + for (const translateField& field : fields) + { + if (rule->m_field == field.field) + { + validator = field.validator; + break; + } + } + if (validator == NULL) + return true; + + // split the input into multiple values and validate every value separately + std::vector<std::string> values = StringUtils::Split(input, RULE_VALUE_SEPARATOR); + for (std::vector<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) + { + if (!validator(*it, data)) + return false; + } + + return true; +} + +bool CSmartPlaylistRule::ValidateRating(const std::string &input, void *data) +{ + char *end = NULL; + std::string strRating = input; + StringUtils::Trim(strRating); + + double rating = std::strtod(strRating.c_str(), &end); + return (end == NULL || *end == '\0') && + rating >= 0.0 && rating <= 10.0; +} + +bool CSmartPlaylistRule::ValidateMyRating(const std::string &input, void *data) +{ + std::string strRating = input; + StringUtils::Trim(strRating); + + int rating = atoi(strRating.c_str()); + return StringValidation::IsPositiveInteger(input, data) && rating <= 10; +} + +std::vector<Field> CSmartPlaylistRule::GetFields(const std::string &type) +{ + std::vector<Field> fields; + bool isVideo = false; + if (type == "mixed") + { + fields.push_back(FieldGenre); + fields.push_back(FieldAlbum); + fields.push_back(FieldArtist); + fields.push_back(FieldAlbumArtist); + fields.push_back(FieldTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldYear); + fields.push_back(FieldTime); + fields.push_back(FieldTrackNumber); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + } + else if (type == "songs") + { + fields.push_back(FieldGenre); + fields.push_back(FieldSource); + fields.push_back(FieldAlbum); + fields.push_back(FieldDiscTitle); + fields.push_back(FieldArtist); + fields.push_back(FieldAlbumArtist); + fields.push_back(FieldTitle); + fields.push_back(FieldYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + fields.push_back(FieldOrigYear); + fields.push_back(FieldTime); + fields.push_back(FieldTrackNumber); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldComment); + fields.push_back(FieldMoods); + fields.push_back(FieldBPM); + fields.push_back(FieldSampleRate); + fields.push_back(FieldMusicBitRate); + fields.push_back(FieldNoOfChannels); + fields.push_back(FieldDateAdded); + fields.push_back(FieldDateModified); + fields.push_back(FieldDateNew); + } + else if (type == "albums") + { + fields.push_back(FieldGenre); + fields.push_back(FieldSource); + fields.push_back(FieldAlbum); + fields.push_back(FieldDiscTitle); + fields.push_back(FieldTotalDiscs); + fields.push_back(FieldIsBoxset); + fields.push_back(FieldArtist); // any artist + fields.push_back(FieldAlbumArtist); // album artist + fields.push_back(FieldYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + fields.push_back(FieldOrigYear); + fields.push_back(FieldAlbumDuration); + fields.push_back(FieldReview); + fields.push_back(FieldThemes); + fields.push_back(FieldMoods); + fields.push_back(FieldStyles); + fields.push_back(FieldCompilation); + fields.push_back(FieldAlbumType); + fields.push_back(FieldMusicLabel); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldPath); + fields.push_back(FieldAlbumStatus); + fields.push_back(FieldDateAdded); + fields.push_back(FieldDateModified); + fields.push_back(FieldDateNew); + } + else if (type == "artists") + { + fields.push_back(FieldArtist); + fields.push_back(FieldSource); + fields.push_back(FieldGenre); + fields.push_back(FieldMoods); + fields.push_back(FieldStyles); + fields.push_back(FieldInstruments); + fields.push_back(FieldBiography); + fields.push_back(FieldArtistType); + fields.push_back(FieldGender); + fields.push_back(FieldDisambiguation); + fields.push_back(FieldBorn); + fields.push_back(FieldBandFormed); + fields.push_back(FieldDisbanded); + fields.push_back(FieldDied); + fields.push_back(FieldRole); + fields.push_back(FieldPath); + fields.push_back(FieldDateAdded); + fields.push_back(FieldDateModified); + fields.push_back(FieldDateNew); + } + else if (type == "tvshows") + { + fields.push_back(FieldTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldPlot); + fields.push_back(FieldTvShowStatus); + fields.push_back(FieldVotes); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldYear); + fields.push_back(FieldGenre); + fields.push_back(FieldDirector); + fields.push_back(FieldActor); + fields.push_back(FieldNumberOfEpisodes); + fields.push_back(FieldNumberOfWatchedEpisodes); + fields.push_back(FieldPlaycount); + fields.push_back(FieldPath); + fields.push_back(FieldStudio); + fields.push_back(FieldMPAA); + fields.push_back(FieldDateAdded); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldInProgress); + fields.push_back(FieldTag); + } + else if (type == "episodes") + { + fields.push_back(FieldTitle); + fields.push_back(FieldTvShowTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldPlot); + fields.push_back(FieldVotes); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldTime); + fields.push_back(FieldWriter); + fields.push_back(FieldAirDate); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldInProgress); + fields.push_back(FieldGenre); + fields.push_back(FieldYear); // premiered + fields.push_back(FieldDirector); + fields.push_back(FieldActor); + fields.push_back(FieldEpisodeNumber); + fields.push_back(FieldSeason); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldStudio); + fields.push_back(FieldMPAA); + fields.push_back(FieldDateAdded); + fields.push_back(FieldTag); + isVideo = true; + } + else if (type == "movies") + { + fields.push_back(FieldTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldPlot); + fields.push_back(FieldPlotOutline); + fields.push_back(FieldTagline); + fields.push_back(FieldVotes); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldTime); + fields.push_back(FieldWriter); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldInProgress); + fields.push_back(FieldGenre); + fields.push_back(FieldCountry); + fields.push_back(FieldYear); // premiered + fields.push_back(FieldDirector); + fields.push_back(FieldActor); + fields.push_back(FieldMPAA); + fields.push_back(FieldTop250); + fields.push_back(FieldStudio); + fields.push_back(FieldTrailer); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldSet); + fields.push_back(FieldTag); + fields.push_back(FieldDateAdded); + isVideo = true; + } + else if (type == "musicvideos") + { + fields.push_back(FieldTitle); + fields.push_back(FieldGenre); + fields.push_back(FieldAlbum); + fields.push_back(FieldYear); + fields.push_back(FieldArtist); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldTime); + fields.push_back(FieldDirector); + fields.push_back(FieldStudio); + fields.push_back(FieldPlot); + fields.push_back(FieldTag); + fields.push_back(FieldDateAdded); + isVideo = true; + } + if (isVideo) + { + fields.push_back(FieldVideoResolution); + fields.push_back(FieldAudioChannels); + fields.push_back(FieldAudioCount); + fields.push_back(FieldSubtitleCount); + fields.push_back(FieldVideoCodec); + fields.push_back(FieldAudioCodec); + fields.push_back(FieldAudioLanguage); + fields.push_back(FieldSubtitleLanguage); + fields.push_back(FieldVideoAspectRatio); + fields.push_back(FieldHdrType); + } + fields.push_back(FieldPlaylist); + fields.push_back(FieldVirtualFolder); + + return fields; +} + +std::vector<SortBy> CSmartPlaylistRule::GetOrders(const std::string &type) +{ + std::vector<SortBy> orders; + orders.push_back(SortByNone); + if (type == "mixed") + { + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByArtist); + orders.push_back(SortByTitle); + orders.push_back(SortByYear); + orders.push_back(SortByTime); + orders.push_back(SortByTrackNumber); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + } + else if (type == "songs") + { + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByArtist); + orders.push_back(SortByTitle); + orders.push_back(SortByYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + orders.push_back(SortByOrigDate); + orders.push_back(SortByTime); + orders.push_back(SortByTrackNumber); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByDateAdded); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByBPM); + } + else if (type == "albums") + { + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByTotalDiscs); + orders.push_back(SortByArtist); // any artist + orders.push_back(SortByYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + orders.push_back(SortByOrigDate); + //orders.push_back(SortByThemes); + //orders.push_back(SortByMoods); + //orders.push_back(SortByStyles); + orders.push_back(SortByAlbumType); + //orders.push_back(SortByMusicLabel); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByDateAdded); + } + else if (type == "artists") + { + orders.push_back(SortByArtist); + } + else if (type == "tvshows") + { + orders.push_back(SortBySortTitle); + orders.push_back(SortByOriginalTitle); + orders.push_back(SortByTvShowStatus); + orders.push_back(SortByVotes); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByYear); + orders.push_back(SortByGenre); + orders.push_back(SortByNumberOfEpisodes); + orders.push_back(SortByNumberOfWatchedEpisodes); + //orders.push_back(SortByPlaycount); + orders.push_back(SortByPath); + orders.push_back(SortByStudio); + orders.push_back(SortByMPAA); + orders.push_back(SortByDateAdded); + orders.push_back(SortByLastPlayed); + } + else if (type == "episodes") + { + orders.push_back(SortByTitle); + orders.push_back(SortByOriginalTitle); + orders.push_back(SortByTvShowTitle); + orders.push_back(SortByVotes); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByTime); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByYear); // premiered/dateaired + orders.push_back(SortByEpisodeNumber); + orders.push_back(SortBySeason); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByStudio); + orders.push_back(SortByMPAA); + orders.push_back(SortByDateAdded); + } + else if (type == "movies") + { + orders.push_back(SortBySortTitle); + orders.push_back(SortByOriginalTitle); + orders.push_back(SortByVotes); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByTime); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByGenre); + orders.push_back(SortByCountry); + orders.push_back(SortByYear); // premiered + orders.push_back(SortByMPAA); + orders.push_back(SortByTop250); + orders.push_back(SortByStudio); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByDateAdded); + } + else if (type == "musicvideos") + { + orders.push_back(SortByTitle); + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByYear); + orders.push_back(SortByArtist); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByTime); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByStudio); + orders.push_back(SortByDateAdded); + } + orders.push_back(SortByRandom); + + return orders; +} + +std::vector<Field> CSmartPlaylistRule::GetGroups(const std::string &type) +{ + std::vector<Field> groups; + groups.push_back(FieldUnknown); + + if (type == "artists") + groups.push_back(FieldGenre); + else if (type == "albums") + { + groups.push_back(FieldYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + groups.push_back(FieldOrigYear); + } + if (type == "movies") + { + groups.push_back(FieldNone); + groups.push_back(FieldSet); + groups.push_back(FieldGenre); + groups.push_back(FieldYear); + groups.push_back(FieldActor); + groups.push_back(FieldDirector); + groups.push_back(FieldWriter); + groups.push_back(FieldStudio); + groups.push_back(FieldCountry); + groups.push_back(FieldTag); + } + else if (type == "tvshows") + { + groups.push_back(FieldGenre); + groups.push_back(FieldYear); + groups.push_back(FieldActor); + groups.push_back(FieldDirector); + groups.push_back(FieldStudio); + groups.push_back(FieldTag); + } + else if (type == "musicvideos") + { + groups.push_back(FieldArtist); + groups.push_back(FieldAlbum); + groups.push_back(FieldGenre); + groups.push_back(FieldYear); + groups.push_back(FieldDirector); + groups.push_back(FieldStudio); + groups.push_back(FieldTag); + } + + return groups; +} + +std::string CSmartPlaylistRule::GetLocalizedGroup(Field group) +{ + for (const auto & i : groups) + { + if (group == i.field) + return g_localizeStrings.Get(i.localizedString); + } + + return g_localizeStrings.Get(groups[0].localizedString); +} + +bool CSmartPlaylistRule::CanGroupMix(Field group) +{ + for (const auto & i : groups) + { + if (group == i.field) + return i.canMix; + } + + return false; +} + +std::string CSmartPlaylistRule::GetLocalizedRule() const +{ + return StringUtils::Format("{} {} {}", GetLocalizedField(m_field), + GetLocalizedOperator(m_operator), GetParameter()); +} + +std::string CSmartPlaylistRule::GetVideoResolutionQuery(const std::string ¶meter) const +{ + std::string retVal(" IN (SELECT DISTINCT idFile FROM streamdetails WHERE iVideoWidth "); + int iRes = (int)std::strtol(parameter.c_str(), NULL, 10); + + int min, max; + if (iRes >= 2160) + { + min = 1921; + max = INT_MAX; + } + else if (iRes >= 1080) { min = 1281; max = 1920; } + else if (iRes >= 720) { min = 961; max = 1280; } + else if (iRes >= 540) { min = 721; max = 960; } + else { min = 0; max = 720; } + + switch (m_operator) + { + case OPERATOR_EQUALS: + retVal += StringUtils::Format(">= {} AND iVideoWidth <= {}", min, max); + break; + case OPERATOR_DOES_NOT_EQUAL: + retVal += StringUtils::Format("< {} OR iVideoWidth > {}", min, max); + break; + case OPERATOR_LESS_THAN: + retVal += StringUtils::Format("< {}", min); + break; + case OPERATOR_GREATER_THAN: + retVal += StringUtils::Format("> {}", max); + break; + default: + break; + } + + retVal += ")"; + return retVal; +} + +std::string CSmartPlaylistRule::GetBooleanQuery(const std::string &negate, const std::string &strType) const +{ + if (strType == "movies") + { + if (m_field == FieldInProgress) + return "movie_view.idFile " + negate + " IN (SELECT DISTINCT idFile FROM bookmark WHERE type = 1)"; + else if (m_field == FieldTrailer) + return negate + GetField(m_field, strType) + "!= ''"; + } + else if (strType == "episodes") + { + if (m_field == FieldInProgress) + return "episode_view.idFile " + negate + " IN (SELECT DISTINCT idFile FROM bookmark WHERE type = 1)"; + } + else if (strType == "tvshows") + { + if (m_field == FieldInProgress) + return negate + " (" + "(tvshow_view.watchedcount > 0 AND tvshow_view.watchedcount < tvshow_view.totalCount) OR " + "(tvshow_view.watchedcount = 0 AND EXISTS " + "(SELECT 1 FROM episode_view WHERE episode_view.idShow = " + GetField(FieldId, strType) + " AND episode_view.resumeTimeInSeconds > 0)" + ")" + ")"; + } + if (strType == "albums") + { + if (m_field == FieldCompilation) + return negate + GetField(m_field, strType); + if (m_field == FieldIsBoxset) + return negate + "albumview.bBoxedSet = 1"; + } + return ""; +} + +CDatabaseQueryRule::SEARCH_OPERATOR CSmartPlaylistRule::GetOperator(const std::string &strType) const +{ + SEARCH_OPERATOR op = CDatabaseQueryRule::GetOperator(strType); + if ((strType == "tvshows" || strType == "episodes") && m_field == FieldYear) + { // special case for premiered which is a date rather than a year + //! @todo SMARTPLAYLISTS do we really need this, or should we just make this field the premiered date and request a date? + if (op == OPERATOR_EQUALS) + op = OPERATOR_CONTAINS; + else if (op == OPERATOR_DOES_NOT_EQUAL) + op = OPERATOR_DOES_NOT_CONTAIN; + } + return op; +} + +std::string CSmartPlaylistRule::FormatParameter(const std::string &operatorString, const std::string ¶m, const CDatabase &db, const std::string &strType) const +{ + // special-casing + if (m_field == FieldTime || m_field == FieldAlbumDuration) + { // translate time to seconds + std::string seconds = std::to_string(StringUtils::TimeStringToSeconds(param)); + return db.PrepareSQL(operatorString, seconds.c_str()); + } + return CDatabaseQueryRule::FormatParameter(operatorString, param, db, strType); +} + +std::string CSmartPlaylistRule::FormatLinkQuery(const char *field, const char *table, const MediaType& mediaType, const std::string& mediaField, const std::string& parameter) +{ + // NOTE: no need for a PrepareSQL here, as the parameter has already been formatted + return StringUtils::Format( + " EXISTS (SELECT 1 FROM {}_link" + " JOIN {} ON {}.{}_id={}_link.{}_id" + " WHERE {}_link.media_id={} AND {}.name {} AND {}_link.media_type = '{}')", + field, table, table, table, field, table, field, mediaField, table, parameter, field, + mediaType); +} + +std::string CSmartPlaylistRule::FormatYearQuery(const std::string& field, + const std::string& param, + const std::string& parameter) const +{ + std::string query; + if (m_operator == OPERATOR_EQUALS && param == "0") + query = "(TRIM(" + field + ") = '' OR " + field + " IS NULL)"; + else if (m_operator == OPERATOR_DOES_NOT_EQUAL && param == "0") + query = "(TRIM(" + field + ") <> '' AND " + field + " IS NOT NULL)"; + else + { // Get year from ISO8601 date string, cast as INTEGER + query = "CAST(" + field + " as INTEGER)" + parameter; + if (m_operator == OPERATOR_LESS_THAN) + query = "(TRIM(" + field + ") = '' OR " + field + " IS NULL OR " + query + ")"; + } + return query; +} + +std::string CSmartPlaylistRule::FormatWhereClause(const std::string &negate, const std::string &oper, const std::string ¶m, + const CDatabase &db, const std::string &strType) const +{ + std::string parameter = FormatParameter(oper, param, db, strType); + + std::string query; + std::string table; + if (strType == "songs") + { + table = "songview"; + + if (m_field == FieldGenre) + query = negate + " EXISTS (SELECT 1 FROM song_genre, genre WHERE song_genre.idSong = " + GetField(FieldId, strType) + " AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")"; + else if (m_field == FieldArtist) + query = negate + " EXISTS (SELECT 1 FROM song_artist, artist WHERE song_artist.idSong = " + GetField(FieldId, strType) + " AND song_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldAlbumArtist) + query = negate + " EXISTS (SELECT 1 FROM album_artist, artist WHERE album_artist.idAlbum = " + table + ".idAlbum AND album_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldLastPlayed && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " is NULL or " + GetField(m_field, strType) + parameter; + else if (m_field == FieldSource) + query = negate + " EXISTS (SELECT 1 FROM album_source, source WHERE album_source.idAlbum = " + table + ".idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + ")"; + else if (m_field == FieldYear || m_field == FieldOrigYear) + { + std::string field; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + field = GetField(FieldOrigYear, strType); + else + field = GetField(m_field, strType); + query = FormatYearQuery(field, param, parameter); + } + } + else if (strType == "albums") + { + table = "albumview"; + + if (m_field == FieldGenre) + query = negate + " EXISTS (SELECT 1 FROM song, song_genre, genre WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND song.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")"; + else if (m_field == FieldArtist) + query = negate + " EXISTS (SELECT 1 FROM song, song_artist, artist WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND song.idSong = song_artist.idSong AND song_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldAlbumArtist) + query = negate + " EXISTS (SELECT 1 FROM album_artist, artist WHERE album_artist.idAlbum = " + GetField(FieldId, strType) + " AND album_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldPath) + query = negate + " EXISTS (SELECT 1 FROM song JOIN path on song.idpath = path.idpath WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND path.strPath" + parameter + ")"; + else if (m_field == FieldLastPlayed && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " is NULL or " + GetField(m_field, strType) + parameter; + else if (m_field == FieldSource) + query = negate + " EXISTS (SELECT 1 FROM album_source, source WHERE album_source.idAlbum = " + GetField(FieldId, strType) + " AND album_source.idSource = source.idSource AND source.strName" + parameter + ")"; + else if (m_field == FieldDiscTitle) + query = negate + + " EXISTS (SELECT 1 FROM song WHERE song.idAlbum = " + GetField(FieldId, strType) + + " AND song.strDiscSubtitle" + parameter + ")"; + else if (m_field == FieldYear || m_field == FieldOrigYear) + { + std::string field; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + field = GetField(FieldOrigYear, strType); + else + field = GetField(m_field, strType); + query = FormatYearQuery(field, param, parameter); + } + } + else if (strType == "artists") + { + table = "artistview"; + + if (m_field == FieldGenre) + { + query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist, song_genre, genre WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song_artist.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")"; + query += " OR "; + query += "EXISTS (SELECT DISTINCT album_artist.idArtist FROM album_artist, song, song_genre, genre WHERE album_artist.idArtist = " + GetField(FieldId, strType) + " AND song.idAlbum = album_artist.idAlbum AND song.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + "))"; + } + else if (m_field == FieldRole) + { + query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist, role WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song_artist.idRole = role.idRole AND role.strRole" + parameter + "))"; + } + else if (m_field == FieldPath) + { + query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong JOIN path ON song.idpath = path.idpath "; + query += "WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND path.strPath" + parameter + "))"; + } + else if (m_field == FieldSource) + { + query = negate + " (EXISTS(SELECT 1 FROM song_artist, song, album_source, source WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song.idSong = song_artist.idSong AND song_artist.idRole = 1 AND album_source.idAlbum = song.idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + ")"; + query += " OR "; + query += " EXISTS (SELECT 1 FROM album_artist, album_source, source WHERE album_artist.idArtist = " + GetField(FieldId, strType) + " AND album_source.idAlbum = album_artist.idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + "))"; + } + } + else if (strType == "movies") + { + table = "movie_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldActor) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldWriter) + query = negate + FormatLinkQuery("writer", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldCountry) + query = negate + FormatLinkQuery("country", "country", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeMovie, GetField(FieldId, strType), parameter); + } + else if (strType == "musicvideos") + { + table = "musicvideo_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if (m_field == FieldArtist || m_field == FieldAlbumArtist) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + } + else if (strType == "tvshows") + { + table = "tvshow_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldActor) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldMPAA) + query = negate + " (" + GetField(m_field, strType) + parameter + ")"; + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldPlaycount) + query = "CASE WHEN COALESCE(" + GetField(FieldNumberOfEpisodes, strType) + " - " + GetField(FieldNumberOfWatchedEpisodes, strType) + ", 0) > 0 THEN 0 ELSE 1 END " + parameter; + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeTvShow, GetField(FieldId, strType), parameter); + } + else if (strType == "episodes") + { + table = "episode_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeTvShow, (table + ".idShow").c_str(), parameter); + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeTvShow, (table + ".idShow").c_str(), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter); + else if (m_field == FieldActor) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter); + else if (m_field == FieldWriter) + query = negate + FormatLinkQuery("writer", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter); + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeTvShow, (table + ".idShow").c_str(), parameter); + else if (m_field == FieldMPAA) + query = negate + " (" + GetField(m_field, strType) + parameter + ")"; + } + if (m_field == FieldVideoResolution) + query = table + ".idFile" + negate + GetVideoResolutionQuery(param); + else if (m_field == FieldAudioChannels) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND iAudioChannels " + parameter + ")"; + else if (m_field == FieldVideoCodec) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strVideoCodec " + parameter + ")"; + else if (m_field == FieldAudioCodec) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strAudioCodec " + parameter + ")"; + else if (m_field == FieldAudioLanguage) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strAudioLanguage " + parameter + ")"; + else if (m_field == FieldSubtitleLanguage) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strSubtitleLanguage " + parameter + ")"; + else if (m_field == FieldVideoAspectRatio) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND fVideoAspect " + parameter + ")"; + else if (m_field == FieldAudioCount) + query = db.PrepareSQL(negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND streamdetails.iStreamtype = %i GROUP BY streamdetails.idFile HAVING COUNT(streamdetails.iStreamType) " + parameter + ")",CStreamDetail::AUDIO); + else if (m_field == FieldSubtitleCount) + query = db.PrepareSQL(negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND streamdetails.iStreamType = %i GROUP BY streamdetails.idFile HAVING COUNT(streamdetails.iStreamType) " + parameter + ")",CStreamDetail::SUBTITLE); + else if (m_field == FieldHdrType) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strHdrType " + parameter + ")"; + if (m_field == FieldPlaycount && strType != "songs" && strType != "albums" && strType != "tvshows") + { // playcount IS stored as NULL OR number IN video db + if ((m_operator == OPERATOR_EQUALS && param == "0") || + (m_operator == OPERATOR_DOES_NOT_EQUAL && param != "0") || + (m_operator == OPERATOR_LESS_THAN)) + { + std::string field = GetField(FieldPlaycount, strType); + query = field + " IS NULL OR " + field + parameter; + } + } + if (query.empty()) + query = CDatabaseQueryRule::FormatWhereClause(negate, oper, param, db, strType); + return query; +} + +std::string CSmartPlaylistRule::GetField(int field, const std::string &type) const +{ + if (field >= FieldUnknown && field < FieldMax) + return DatabaseUtils::GetField((Field)field, CMediaTypes::FromString(type), DatabaseQueryPartWhere); + return ""; +} + +std::string CSmartPlaylistRuleCombination::GetWhereClause(const CDatabase &db, const std::string& strType, std::set<std::string> &referencedPlaylists) const +{ + std::string rule; + + // translate the combinations into SQL + for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin(); it != m_combinations.end(); ++it) + { + if (it != m_combinations.begin()) + rule += m_type == CombinationAnd ? " AND " : " OR "; + std::shared_ptr<CSmartPlaylistRuleCombination> combo = std::static_pointer_cast<CSmartPlaylistRuleCombination>(*it); + if (combo) + rule += "(" + combo->GetWhereClause(db, strType, referencedPlaylists) + ")"; + } + + // translate the rules into SQL + for (CDatabaseQueryRules::const_iterator it = m_rules.begin(); it != m_rules.end(); ++it) + { + // don't include playlists that are meant to be displayed + // as a virtual folders in the SQL WHERE clause + if ((*it)->m_field == FieldVirtualFolder) + continue; + + if (!rule.empty()) + rule += m_type == CombinationAnd ? " AND " : " OR "; + rule += "("; + std::string currentRule; + if ((*it)->m_field == FieldPlaylist) + { + std::string playlistFile = CSmartPlaylistDirectory::GetPlaylistByName((*it)->m_parameter.at(0), strType); + if (!playlistFile.empty() && referencedPlaylists.find(playlistFile) == referencedPlaylists.end()) + { + referencedPlaylists.insert(playlistFile); + CSmartPlaylist playlist; + if (playlist.Load(playlistFile)) + { + std::string playlistQuery; + // only playlists of same type will be part of the query + if (playlist.GetType() == strType || (playlist.GetType() == "mixed" && (strType == "songs" || strType == "musicvideos")) || playlist.GetType().empty()) + { + playlist.SetType(strType); + playlistQuery = playlist.GetWhereClause(db, referencedPlaylists); + } + if (playlist.GetType() == strType) + { + if ((*it)->m_operator == CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL) + currentRule = StringUtils::Format("NOT ({})", playlistQuery); + else + currentRule = playlistQuery; + } + } + } + } + else + currentRule = (*it)->GetWhereClause(db, strType); + // if we don't get a rule, we add '1' or '0' so the query is still valid and doesn't fail + if (currentRule.empty()) + currentRule = m_type == CombinationAnd ? "'1'" : "'0'"; + rule += currentRule; + rule += ")"; + } + + return rule; +} + +void CSmartPlaylistRuleCombination::GetVirtualFolders(const std::string& strType, std::vector<std::string> &virtualFolders) const +{ + for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin(); it != m_combinations.end(); ++it) + { + std::shared_ptr<CSmartPlaylistRuleCombination> combo = std::static_pointer_cast<CSmartPlaylistRuleCombination>(*it); + if (combo) + combo->GetVirtualFolders(strType, virtualFolders); + } + + for (CDatabaseQueryRules::const_iterator it = m_rules.begin(); it != m_rules.end(); ++it) + { + if (((*it)->m_field != FieldVirtualFolder && (*it)->m_field != FieldPlaylist) || (*it)->m_operator != CDatabaseQueryRule::OPERATOR_EQUALS) + continue; + + std::string playlistFile = CSmartPlaylistDirectory::GetPlaylistByName((*it)->m_parameter.at(0), strType); + if (playlistFile.empty()) + continue; + + if ((*it)->m_field == FieldVirtualFolder) + virtualFolders.push_back(playlistFile); + else + { + // look for any virtual folders in the expanded playlists + CSmartPlaylist playlist; + if (!playlist.Load(playlistFile)) + continue; + + if (CSmartPlaylist::CheckTypeCompatibility(playlist.GetType(), strType)) + playlist.GetVirtualFolders(virtualFolders); + } + } +} + +void CSmartPlaylistRuleCombination::AddRule(const CSmartPlaylistRule &rule) +{ + std::shared_ptr<CSmartPlaylistRule> ptr(new CSmartPlaylistRule(rule)); + m_rules.push_back(ptr); +} + +CSmartPlaylist::CSmartPlaylist() +{ + Reset(); +} + +bool CSmartPlaylist::OpenAndReadName(const CURL &url) +{ + if (readNameFromPath(url) == NULL) + return false; + + return !m_playlistName.empty(); +} + +const TiXmlNode* CSmartPlaylist::readName(const TiXmlNode *root) +{ + if (root == NULL) + return NULL; + + const TiXmlElement *rootElem = root->ToElement(); + if (rootElem == NULL) + return NULL; + + if (!StringUtils::EqualsNoCase(root->Value(), "smartplaylist")) + { + CLog::Log(LOGERROR, "Error loading Smart playlist"); + return NULL; + } + + // load the playlist type + const char* type = rootElem->Attribute("type"); + if (type) + m_playlistType = type; + // backward compatibility: + if (m_playlistType == "music") + m_playlistType = "songs"; + if (m_playlistType == "video") + m_playlistType = "musicvideos"; + + // load the playlist name + XMLUtils::GetString(root, "name", m_playlistName); + + return root; +} + +const TiXmlNode* CSmartPlaylist::readNameFromPath(const CURL &url) +{ + CFileStream file; + if (!file.Open(url)) + { + CLog::Log(LOGERROR, "Error loading Smart playlist {} (failed to read file)", url.GetRedacted()); + return NULL; + } + + m_xmlDoc.Clear(); + file >> m_xmlDoc; + + const TiXmlNode *root = readName(m_xmlDoc.RootElement()); + if (m_playlistName.empty()) + { + m_playlistName = CUtil::GetTitleFromPath(url.Get()); + if (URIUtils::HasExtension(m_playlistName, ".xsp")) + URIUtils::RemoveExtension(m_playlistName); + } + + return root; +} + +const TiXmlNode* CSmartPlaylist::readNameFromXml(const std::string &xml) +{ + if (xml.empty()) + { + CLog::Log(LOGERROR, "Error loading empty Smart playlist"); + return NULL; + } + + m_xmlDoc.Clear(); + if (!m_xmlDoc.Parse(xml)) + { + CLog::Log(LOGERROR, "Error loading Smart playlist (failed to parse xml: {})", + m_xmlDoc.ErrorDesc()); + return NULL; + } + + const TiXmlNode *root = readName(m_xmlDoc.RootElement()); + + return root; +} + +bool CSmartPlaylist::load(const TiXmlNode *root) +{ + if (root == NULL) + return false; + + return LoadFromXML(root); +} + +bool CSmartPlaylist::Load(const CURL &url) +{ + return load(readNameFromPath(url)); +} + +bool CSmartPlaylist::Load(const std::string &path) +{ + const CURL pathToUrl(path); + return load(readNameFromPath(pathToUrl)); +} + +bool CSmartPlaylist::Load(const CVariant &obj) +{ + if (!obj.isObject()) + return false; + + // load the playlist type + if (obj.isMember("type") && obj["type"].isString()) + m_playlistType = obj["type"].asString(); + + // backward compatibility + if (m_playlistType == "music") + m_playlistType = "songs"; + if (m_playlistType == "video") + m_playlistType = "musicvideos"; + + // load the playlist name + if (obj.isMember("name") && obj["name"].isString()) + m_playlistName = obj["name"].asString(); + + if (obj.isMember("rules")) + m_ruleCombination.Load(obj["rules"], this); + + if (obj.isMember("group") && obj["group"].isMember("type") && obj["group"]["type"].isString()) + { + m_group = obj["group"]["type"].asString(); + if (obj["group"].isMember("mixed") && obj["group"]["mixed"].isBoolean()) + m_groupMixed = obj["group"]["mixed"].asBoolean(); + } + + // now any limits + if (obj.isMember("limit") && (obj["limit"].isInteger() || obj["limit"].isUnsignedInteger()) && obj["limit"].asUnsignedInteger() > 0) + m_limit = (unsigned int)obj["limit"].asUnsignedInteger(); + + // and order + if (obj.isMember("order") && obj["order"].isMember("method") && obj["order"]["method"].isString()) + { + const CVariant &order = obj["order"]; + if (order.isMember("direction") && order["direction"].isString()) + m_orderDirection = StringUtils::EqualsNoCase(order["direction"].asString(), "ascending") ? SortOrderAscending : SortOrderDescending; + + if (order.isMember("ignorefolders") && obj["ignorefolders"].isBoolean()) + m_orderAttributes = obj["ignorefolders"].asBoolean() ? SortAttributeIgnoreFolders : SortAttributeNone; + + m_orderField = CSmartPlaylistRule::TranslateOrder(obj["order"]["method"].asString().c_str()); + } + + return true; +} + +bool CSmartPlaylist::LoadFromXml(const std::string &xml) +{ + return load(readNameFromXml(xml)); +} + +bool CSmartPlaylist::LoadFromXML(const TiXmlNode *root, const std::string &encoding) +{ + if (!root) + return false; + + std::string tmp; + if (XMLUtils::GetString(root, "match", tmp)) + m_ruleCombination.SetType(StringUtils::EqualsNoCase(tmp, "all") ? CSmartPlaylistRuleCombination::CombinationAnd : CSmartPlaylistRuleCombination::CombinationOr); + + // now the rules + const TiXmlNode *ruleNode = root->FirstChild("rule"); + while (ruleNode) + { + CSmartPlaylistRule rule; + if (rule.Load(ruleNode, encoding)) + m_ruleCombination.AddRule(rule); + + ruleNode = ruleNode->NextSibling("rule"); + } + + const TiXmlElement *groupElement = root->FirstChildElement("group"); + if (groupElement != NULL && groupElement->FirstChild() != NULL) + { + m_group = groupElement->FirstChild()->ValueStr(); + const char* mixed = groupElement->Attribute("mixed"); + m_groupMixed = mixed != NULL && StringUtils::EqualsNoCase(mixed, "true"); + } + + // now any limits + // format is <limit>25</limit> + XMLUtils::GetUInt(root, "limit", m_limit); + + // and order + // format is <order direction="ascending">field</order> + const TiXmlElement *order = root->FirstChildElement("order"); + if (order && order->FirstChild()) + { + const char *direction = order->Attribute("direction"); + if (direction) + m_orderDirection = StringUtils::EqualsNoCase(direction, "ascending") ? SortOrderAscending : SortOrderDescending; + + const char *ignorefolders = order->Attribute("ignorefolders"); + if (ignorefolders != NULL) + m_orderAttributes = StringUtils::EqualsNoCase(ignorefolders, "true") ? SortAttributeIgnoreFolders : SortAttributeNone; + + m_orderField = CSmartPlaylistRule::TranslateOrder(order->FirstChild()->Value()); + } + return true; +} + +bool CSmartPlaylist::LoadFromJson(const std::string &json) +{ + if (json.empty()) + return false; + + CVariant obj; + if (!CJSONVariantParser::Parse(json, obj)) + return false; + + return Load(obj); +} + +bool CSmartPlaylist::Save(const std::string &path) const +{ + CXBMCTinyXML doc; + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + doc.InsertEndChild(decl); + + TiXmlElement xmlRootElement("smartplaylist"); + xmlRootElement.SetAttribute("type",m_playlistType.c_str()); + TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement); + if (!pRoot) + return false; + + // add the <name> tag + XMLUtils::SetString(pRoot, "name", m_playlistName); + + // add the <match> tag + XMLUtils::SetString(pRoot, "match", m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationAnd ? "all" : "one"); + + // add <rule> tags + m_ruleCombination.Save(pRoot); + + // add <group> tag if necessary + if (!m_group.empty()) + { + TiXmlElement nodeGroup("group"); + if (m_groupMixed) + nodeGroup.SetAttribute("mixed", "true"); + TiXmlText group(m_group.c_str()); + nodeGroup.InsertEndChild(group); + pRoot->InsertEndChild(nodeGroup); + } + + // add <limit> tag + if (m_limit) + XMLUtils::SetInt(pRoot, "limit", m_limit); + + // add <order> tag + if (m_orderField != SortByNone) + { + TiXmlText order(CSmartPlaylistRule::TranslateOrder(m_orderField).c_str()); + TiXmlElement nodeOrder("order"); + nodeOrder.SetAttribute("direction", m_orderDirection == SortOrderDescending ? "descending" : "ascending"); + if (m_orderAttributes & SortAttributeIgnoreFolders) + nodeOrder.SetAttribute("ignorefolders", "true"); + nodeOrder.InsertEndChild(order); + pRoot->InsertEndChild(nodeOrder); + } + return doc.SaveFile(path); +} + +bool CSmartPlaylist::Save(CVariant &obj, bool full /* = true */) const +{ + if (obj.type() == CVariant::VariantTypeConstNull) + return false; + + obj.clear(); + // add "type" + obj["type"] = m_playlistType; + + // add "rules" + CVariant rulesObj = CVariant(CVariant::VariantTypeObject); + if (m_ruleCombination.Save(rulesObj)) + obj["rules"] = rulesObj; + + // add "group" + if (!m_group.empty()) + { + obj["group"]["type"] = m_group; + obj["group"]["mixed"] = m_groupMixed; + } + + // add "limit" + if (full && m_limit) + obj["limit"] = m_limit; + + // add "order" + if (full && m_orderField != SortByNone) + { + obj["order"] = CVariant(CVariant::VariantTypeObject); + obj["order"]["method"] = CSmartPlaylistRule::TranslateOrder(m_orderField); + obj["order"]["direction"] = m_orderDirection == SortOrderDescending ? "descending" : "ascending"; + obj["order"]["ignorefolders"] = (m_orderAttributes & SortAttributeIgnoreFolders); + } + + return true; +} + +bool CSmartPlaylist::SaveAsJson(std::string &json, bool full /* = true */) const +{ + CVariant xsp(CVariant::VariantTypeObject); + if (!Save(xsp, full)) + return false; + + return CJSONVariantWriter::Write(xsp, json, true) && !json.empty(); +} + +void CSmartPlaylist::Reset() +{ + m_ruleCombination.clear(); + m_limit = 0; + m_orderField = SortByNone; + m_orderDirection = SortOrderNone; + m_orderAttributes = SortAttributeNone; + m_playlistType = "songs"; // sane default + m_group.clear(); + m_groupMixed = false; +} + +void CSmartPlaylist::SetName(const std::string &name) +{ + m_playlistName = name; +} + +void CSmartPlaylist::SetType(const std::string &type) +{ + m_playlistType = type; +} + +bool CSmartPlaylist::IsVideoType() const +{ + return IsVideoType(m_playlistType); +} + +bool CSmartPlaylist::IsMusicType() const +{ + return IsMusicType(m_playlistType); +} + +bool CSmartPlaylist::IsVideoType(const std::string &type) +{ + return type == "movies" || type == "tvshows" || type == "episodes" || + type == "musicvideos" || type == "mixed"; +} + +bool CSmartPlaylist::IsMusicType(const std::string &type) +{ + return type == "artists" || type == "albums" || + type == "songs" || type == "mixed"; +} + +std::string CSmartPlaylist::GetWhereClause(const CDatabase &db, std::set<std::string> &referencedPlaylists) const +{ + return m_ruleCombination.GetWhereClause(db, GetType(), referencedPlaylists); +} + +void CSmartPlaylist::GetVirtualFolders(std::vector<std::string> &virtualFolders) const +{ + m_ruleCombination.GetVirtualFolders(GetType(), virtualFolders); +} + +std::string CSmartPlaylist::GetSaveLocation() const +{ + if (m_playlistType == "mixed") + return "mixed"; + if (IsMusicType()) + return "music"; + // all others are video + return "video"; +} + +void CSmartPlaylist::GetAvailableFields(const std::string &type, std::vector<std::string> &fieldList) +{ + std::vector<Field> typeFields = CSmartPlaylistRule::GetFields(type); + for (std::vector<Field>::const_iterator field = typeFields.begin(); field != typeFields.end(); ++field) + { + for (const translateField& i : fields) + { + if (*field == i.field) + fieldList.emplace_back(i.string); + } + } +} + +bool CSmartPlaylist::IsEmpty(bool ignoreSortAndLimit /* = true */) const +{ + bool empty = m_ruleCombination.empty(); + if (empty && !ignoreSortAndLimit) + empty = m_limit <= 0 && m_orderField == SortByNone && m_orderDirection == SortOrderNone; + + return empty; +} + +bool CSmartPlaylist::CheckTypeCompatibility(const std::string &typeLeft, const std::string &typeRight) +{ + if (typeLeft == typeRight) + return true; + + if (typeLeft == "mixed" && + (typeRight == "songs" || typeRight == "musicvideos")) + return true; + + if (typeRight == "mixed" && + (typeLeft == "songs" || typeLeft == "musicvideos")) + return true; + + return false; +} + +CDatabaseQueryRule *CSmartPlaylist::CreateRule() const +{ + return new CSmartPlaylistRule(); +} +CDatabaseQueryRuleCombination *CSmartPlaylist::CreateCombination() const +{ + return new CSmartPlaylistRuleCombination(); +} diff --git a/xbmc/playlists/SmartPlayList.h b/xbmc/playlists/SmartPlayList.h new file mode 100644 index 0000000..2a153c3 --- /dev/null +++ b/xbmc/playlists/SmartPlayList.h @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "dbwrappers/DatabaseQuery.h" +#include "utils/SortUtils.h" +#include "utils/XBMCTinyXML.h" + +#include <memory> +#include <set> +#include <string> +#include <vector> + +class CURL; +class CVariant; + +class CSmartPlaylistRule : public CDatabaseQueryRule +{ +public: + CSmartPlaylistRule(); + ~CSmartPlaylistRule() override = default; + + std::string GetLocalizedRule() const; + + static SortBy TranslateOrder(const char *order); + static std::string TranslateOrder(SortBy order); + static Field TranslateGroup(const char *group); + static std::string TranslateGroup(Field group); + + static std::string GetLocalizedField(int field); + static std::string GetLocalizedGroup(Field group); + static bool CanGroupMix(Field group); + + static std::vector<Field> GetFields(const std::string &type); + static std::vector<SortBy> GetOrders(const std::string &type); + static std::vector<Field> GetGroups(const std::string &type); + FIELD_TYPE GetFieldType(int field) const override; + static bool IsFieldBrowseable(int field); + + static bool Validate(const std::string &input, void *data); + static bool ValidateRating(const std::string &input, void *data); + static bool ValidateMyRating(const std::string &input, void *data); + +protected: + std::string GetField(int field, const std::string& type) const override; + int TranslateField(const char *field) const override; + std::string TranslateField(int field) const override; + std::string FormatParameter(const std::string &negate, + const std::string &oper, + const CDatabase &db, + const std::string &type) const override; + std::string FormatWhereClause(const std::string &negate, + const std::string& oper, + const std::string ¶m, + const CDatabase &db, + const std::string &type) const override; + SEARCH_OPERATOR GetOperator(const std::string &type) const override; + std::string GetBooleanQuery(const std::string &negate, + const std::string &strType) const override; + +private: + std::string GetVideoResolutionQuery(const std::string ¶meter) const; + static std::string FormatLinkQuery(const char *field, const char *table, const MediaType& mediaType, const std::string& mediaField, const std::string& parameter); + std::string FormatYearQuery(const std::string& field, + const std::string& param, + const std::string& parameter) const; +}; + +class CSmartPlaylistRuleCombination : public CDatabaseQueryRuleCombination +{ +public: + CSmartPlaylistRuleCombination() = default; + ~CSmartPlaylistRuleCombination() override = default; + + std::string GetWhereClause(const CDatabase &db, + const std::string& strType, + std::set<std::string> &referencedPlaylists) const; + void GetVirtualFolders(const std::string& strType, + std::vector<std::string> &virtualFolders) const; + + void AddRule(const CSmartPlaylistRule &rule); +}; + +class CSmartPlaylist : public IDatabaseQueryRuleFactory +{ +public: + CSmartPlaylist(); + ~CSmartPlaylist() override = default; + + bool Load(const CURL& url); + bool Load(const std::string &path); + bool Load(const CVariant &obj); + bool LoadFromXml(const std::string &xml); + bool LoadFromJson(const std::string &json); + bool Save(const std::string &path) const; + bool Save(CVariant &obj, bool full = true) const; + bool SaveAsJson(std::string &json, bool full = true) const; + + bool OpenAndReadName(const CURL &url); + bool LoadFromXML(const TiXmlNode *root, const std::string &encoding = "UTF-8"); + + void Reset(); + + void SetName(const std::string &name); + void SetType(const std::string &type); // music, video, mixed + const std::string& GetName() const { return m_playlistName; } + const std::string& GetType() const { return m_playlistType; } + bool IsVideoType() const; + bool IsMusicType() const; + + void SetMatchAllRules(bool matchAll) { m_ruleCombination.SetType(matchAll ? CSmartPlaylistRuleCombination::CombinationAnd : CSmartPlaylistRuleCombination::CombinationOr); } + bool GetMatchAllRules() const { return m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationAnd; } + + void SetLimit(unsigned int limit) { m_limit = limit; } + unsigned int GetLimit() const { return m_limit; } + + void SetOrder(SortBy order) { m_orderField = order; } + SortBy GetOrder() const { return m_orderField; } + void SetOrderAscending(bool orderAscending) + { + m_orderDirection = orderAscending ? SortOrderAscending : SortOrderDescending; + } + bool GetOrderAscending() const { return m_orderDirection != SortOrderDescending; } + SortOrder GetOrderDirection() const { return m_orderDirection; } + void SetOrderAttributes(SortAttribute attributes) { m_orderAttributes = attributes; } + SortAttribute GetOrderAttributes() const { return m_orderAttributes; } + + void SetGroup(const std::string &group) { m_group = group; } + const std::string& GetGroup() const { return m_group; } + void SetGroupMixed(bool mixed) { m_groupMixed = mixed; } + bool IsGroupMixed() const { return m_groupMixed; } + + /*! \brief get the where clause for a playlist + We handle playlists inside playlists separately in order to ensure we don't introduce infinite loops + by playlist A including playlist B which also (perhaps via other playlists) then includes playlistA. + + \param db the database to use to format up results + \param referencedPlaylists a set of playlists to know when we reach a cycle + \param needWhere whether we need to prepend the where clause with "WHERE " + */ + std::string GetWhereClause(const CDatabase &db, std::set<std::string> &referencedPlaylists) const; + void GetVirtualFolders(std::vector<std::string> &virtualFolders) const; + + std::string GetSaveLocation() const; + + static void GetAvailableFields(const std::string &type, std::vector<std::string> &fieldList); + + static bool IsVideoType(const std::string &type); + static bool IsMusicType(const std::string &type); + static bool CheckTypeCompatibility(const std::string &typeLeft, const std::string &typeRight); + + bool IsEmpty(bool ignoreSortAndLimit = true) const; + + // rule creation + CDatabaseQueryRule *CreateRule() const override; + CDatabaseQueryRuleCombination *CreateCombination() const override; +private: + friend class CGUIDialogSmartPlaylistEditor; + friend class CGUIDialogMediaFilter; + + const TiXmlNode* readName(const TiXmlNode *root); + const TiXmlNode* readNameFromPath(const CURL &url); + const TiXmlNode* readNameFromXml(const std::string &xml); + bool load(const TiXmlNode *root); + + CSmartPlaylistRuleCombination m_ruleCombination; + std::string m_playlistName; + std::string m_playlistType; + + // order information + unsigned int m_limit; + SortBy m_orderField; + SortOrder m_orderDirection; + SortAttribute m_orderAttributes; + std::string m_group; + bool m_groupMixed; + + CXBMCTinyXML m_xmlDoc; +}; + diff --git a/xbmc/playlists/SmartPlaylistFileItemListModifier.cpp b/xbmc/playlists/SmartPlaylistFileItemListModifier.cpp new file mode 100644 index 0000000..7249bb5 --- /dev/null +++ b/xbmc/playlists/SmartPlaylistFileItemListModifier.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SmartPlaylistFileItemListModifier.h" + +#include "FileItem.h" +#include "URL.h" +#include "playlists/SmartPlayList.h" +#include "utils/StringUtils.h" + +#include <string> + +#define URL_OPTION_XSP "xsp" +#define PROPERTY_SORT_ORDER "sort.order" +#define PROPERTY_SORT_ASCENDING "sort.ascending" + +bool CSmartPlaylistFileItemListModifier::CanModify(const CFileItemList &items) const +{ + return !GetUrlOption(items.GetPath(), URL_OPTION_XSP).empty(); +} + +bool CSmartPlaylistFileItemListModifier::Modify(CFileItemList &items) const +{ + if (items.HasProperty(PROPERTY_SORT_ORDER)) + return false; + + std::string xspOption = GetUrlOption(items.GetPath(), URL_OPTION_XSP); + if (xspOption.empty()) + return false; + + // check for smartplaylist-specific sorting information + CSmartPlaylist xsp; + if (!xsp.LoadFromJson(xspOption)) + return false; + + items.SetProperty(PROPERTY_SORT_ORDER, (int)xsp.GetOrder()); + items.SetProperty(PROPERTY_SORT_ASCENDING, xsp.GetOrderDirection() == SortOrderAscending); + + return true; +} + +std::string CSmartPlaylistFileItemListModifier::GetUrlOption(const std::string &path, const std::string &option) +{ + if (path.empty() || option.empty()) + return StringUtils::Empty; + + CURL url(path); + return url.GetOption(option); +} diff --git a/xbmc/playlists/SmartPlaylistFileItemListModifier.h b/xbmc/playlists/SmartPlaylistFileItemListModifier.h new file mode 100644 index 0000000..5d64084 --- /dev/null +++ b/xbmc/playlists/SmartPlaylistFileItemListModifier.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "IFileItemListModifier.h" + +#include <string> + +class CSmartPlaylistFileItemListModifier : public IFileItemListModifier +{ +public: + CSmartPlaylistFileItemListModifier() = default; + ~CSmartPlaylistFileItemListModifier() override = default; + + bool CanModify(const CFileItemList &items) const override; + bool Modify(CFileItemList &items) const override; + +private: + static std::string GetUrlOption(const std::string &path, const std::string &option); +}; diff --git a/xbmc/playlists/test/CMakeLists.txt b/xbmc/playlists/test/CMakeLists.txt new file mode 100644 index 0000000..f421fac --- /dev/null +++ b/xbmc/playlists/test/CMakeLists.txt @@ -0,0 +1,4 @@ +set(SOURCES TestPlayListFactory.cpp + TestPlayListXSPF.cpp) + +core_add_test_library(playlists_test) diff --git a/xbmc/playlists/test/TestPlayListFactory.cpp b/xbmc/playlists/test/TestPlayListFactory.cpp new file mode 100644 index 0000000..08b2b4b --- /dev/null +++ b/xbmc/playlists/test/TestPlayListFactory.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 Tyler Szabo + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "URL.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#include "playlists/PlayListXSPF.h" +#include "test/TestUtils.h" + +#include <gtest/gtest.h> + +using namespace PLAYLIST; + + +TEST(TestPlayListFactory, XSPF) +{ + std::string filename = XBMC_REF_FILE_PATH("/xbmc/playlists/test/newfile.xspf"); + CURL url("http://example.com/playlists/playlist.xspf"); + CPlayList* playlist = nullptr; + + EXPECT_TRUE(CPlayListFactory::IsPlaylist(url)); + EXPECT_TRUE(CPlayListFactory::IsPlaylist(filename)); + + playlist = CPlayListFactory::Create(filename); + EXPECT_NE(playlist, nullptr); + + if (playlist) + { + EXPECT_NE(dynamic_cast<CPlayListXSPF*>(playlist), nullptr); + delete playlist; + } +} diff --git a/xbmc/playlists/test/TestPlayListXSPF.cpp b/xbmc/playlists/test/TestPlayListXSPF.cpp new file mode 100644 index 0000000..574d05d --- /dev/null +++ b/xbmc/playlists/test/TestPlayListXSPF.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 Tyler Szabo + * Copyright (C) 2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "FileItem.h" +#include "URL.h" +#include "playlists/PlayListXSPF.h" +#include "test/TestUtils.h" +#include "utils/URIUtils.h" + +#include <gtest/gtest.h> + +using namespace PLAYLIST; + + +TEST(TestPlayListXSPF, Load) +{ + std::string filename = XBMC_REF_FILE_PATH("/xbmc/playlists/test/test.xspf"); + CPlayListXSPF playlist; + std::vector<std::string> pathparts; + std::vector<std::string>::reverse_iterator it; + + EXPECT_TRUE(playlist.Load(filename)); + + EXPECT_EQ(playlist.size(), 5); + EXPECT_STREQ(playlist.GetName().c_str(), "Various Music"); + + + ASSERT_GT(playlist.size(), 0); + EXPECT_STREQ(playlist[0]->GetLabel().c_str(), ""); + EXPECT_STREQ(playlist[0]->GetURL().Get().c_str(), "http://example.com/song_1.mp3"); + + + ASSERT_GT(playlist.size(), 1); + EXPECT_STREQ(playlist[1]->GetLabel().c_str(), "Relative local file"); + pathparts = URIUtils::SplitPath(playlist[1]->GetPath()); + it = pathparts.rbegin(); + EXPECT_STREQ((*it++).c_str(), "song_2.mp3"); + EXPECT_STREQ((*it++).c_str(), "path_to"); + EXPECT_STREQ((*it++).c_str(), "relative"); + EXPECT_STREQ((*it++).c_str(), "test"); + EXPECT_STREQ((*it++).c_str(), "playlists"); + EXPECT_STREQ((*it++).c_str(), "xbmc"); + + + ASSERT_GT(playlist.size(), 2); + EXPECT_STREQ(playlist[2]->GetLabel().c_str(), "Don\xC2\x92t Worry, We\xC2\x92ll Be Watching You"); + pathparts = URIUtils::SplitPath(playlist[2]->GetPath()); + it = pathparts.rbegin(); + EXPECT_STREQ((*it++).c_str(), "09 - Don't Worry, We'll Be Watching You.mp3"); + EXPECT_STREQ((*it++).c_str(), "Making Mirrors"); + EXPECT_STREQ((*it++).c_str(), "Gotye"); + EXPECT_STREQ((*it++).c_str(), "Music"); + EXPECT_STREQ((*it++).c_str(), "jane"); + EXPECT_STREQ((*it++).c_str(), "Users"); + EXPECT_STREQ((*it++).c_str(), "C:"); + + + ASSERT_GT(playlist.size(), 3); + EXPECT_STREQ(playlist[3]->GetLabel().c_str(), "Rollin' & Scratchin'"); + pathparts = URIUtils::SplitPath(playlist[3]->GetPath()); + it = pathparts.rbegin(); + EXPECT_STREQ((*it++).c_str(), "08 - Rollin' & Scratchin'.mp3"); + EXPECT_STREQ((*it++).c_str(), "Homework"); + EXPECT_STREQ((*it++).c_str(), "Daft Punk"); + EXPECT_STREQ((*it++).c_str(), "Music"); + EXPECT_STREQ((*it++).c_str(), "jane"); + EXPECT_STREQ((*it++).c_str(), "home"); + + + ASSERT_GT(playlist.size(), 4); + EXPECT_STREQ(playlist[4]->GetLabel().c_str(), ""); + EXPECT_STREQ(playlist[4]->GetURL().Get().c_str(), "http://example.com/song_2.mp3"); +} diff --git a/xbmc/playlists/test/test.xspf b/xbmc/playlists/test/test.xspf new file mode 100644 index 0000000..cf98d64 --- /dev/null +++ b/xbmc/playlists/test/test.xspf @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<playlist version="1" xmlns="http://xspf.org/ns/0/"> + + <!-- title of the playlist --> + <title>Various Music</title> + + <!-- name of the author --> + <creator>Jane Doe</creator> + + <!-- homepage of the author --> + <info>http://example.com/~jane</info> + + <trackList> + <track> + <location>http://example.com/song_1.mp3</location> + </track> + + <track> + <title>Relative local file</title> + <location>relative/path_to/song_2.mp3</location> + </track> + + <track> + <!--Absolute Windows path --> + <location>file:///C:/Users/jane/Music/Gotye/Making%20Mirrors/09%20-%20Don%27t%20Worry%2C%20We%27ll%20Be%20Watching%20You.mp3</location> + <title>Don’t Worry, We’ll Be Watching You</title> + <creator>Gotye</creator> + <album>Making Mirrors</album> + <trackNum>9</trackNum> + </track> + + <track> + <!--Absolute Linux path --> + <location>file:///home/jane/Music/Daft%20Punk/Homework/08%20-%20Rollin%27%20%26%20Scratchin%27.mp3</location> + <title>Rollin' & Scratchin'</title> + <creator>Daft Punk</creator> + <album>Homework</album> + <trackNum>8</trackNum> + </track> + + <track> + <title></title> + <location></location> + </track> + + <track> + <title>Nothing to add for this entry</title> + </track> + + <track> + <location>http://example.com/song_2.mp3</location> + </track> + </trackList> +</playlist> |