summaryrefslogtreecommitdiffstats
path: root/xbmc/playlists
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xbmc/playlists/CMakeLists.txt26
-rw-r--r--xbmc/playlists/PlayList.cpp513
-rw-r--r--xbmc/playlists/PlayList.h92
-rw-r--r--xbmc/playlists/PlayListB4S.cpp133
-rw-r--r--xbmc/playlists/PlayListB4S.h25
-rw-r--r--xbmc/playlists/PlayListFactory.cpp145
-rw-r--r--xbmc/playlists/PlayListFactory.h29
-rw-r--r--xbmc/playlists/PlayListM3U.cpp278
-rw-r--r--xbmc/playlists/PlayListM3U.h38
-rw-r--r--xbmc/playlists/PlayListPLS.cpp425
-rw-r--r--xbmc/playlists/PlayListPLS.h44
-rw-r--r--xbmc/playlists/PlayListTypes.h31
-rw-r--r--xbmc/playlists/PlayListURL.cpp69
-rw-r--r--xbmc/playlists/PlayListURL.h23
-rw-r--r--xbmc/playlists/PlayListWPL.cpp130
-rw-r--r--xbmc/playlists/PlayListWPL.h25
-rw-r--r--xbmc/playlists/PlayListXML.cpp197
-rw-r--r--xbmc/playlists/PlayListXML.h24
-rw-r--r--xbmc/playlists/PlayListXSPF.cpp132
-rw-r--r--xbmc/playlists/PlayListXSPF.h24
-rw-r--r--xbmc/playlists/SmartPlayList.cpp1610
-rw-r--r--xbmc/playlists/SmartPlayList.h186
-rw-r--r--xbmc/playlists/SmartPlaylistFileItemListModifier.cpp54
-rw-r--r--xbmc/playlists/SmartPlaylistFileItemListModifier.h26
-rw-r--r--xbmc/playlists/test/CMakeLists.txt4
-rw-r--r--xbmc/playlists/test/TestPlayListFactory.cpp38
-rw-r--r--xbmc/playlists/test/TestPlayListXSPF.cpp79
-rw-r--r--xbmc/playlists/test/test.xspf54
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 &parameter) 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 &param, 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 &param,
+ 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 &param,
+ 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 &parameter) 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&#146;t Worry, We&#146;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&#39; &amp; Scratchin&#39;</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>