Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[playlist] Add support for reading XSPF Playlists #14585

Merged
merged 1 commit into from Oct 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake/installdata/test-reference-data.txt
Expand Up @@ -8,3 +8,4 @@ xbmc/filesystem/test/refRARstored.rar
xbmc/network/test/data/test.html
xbmc/network/test/data/test.png
xbmc/network/test/data/test-ranges.txt
xbmc/playlists/test/test.xspf
1 change: 1 addition & 0 deletions cmake/treedata/common/tests.txt
Expand Up @@ -4,6 +4,7 @@ xbmc/filesystem/test test/filesystem
xbmc/interfaces/python/test test/python
xbmc/music/tags/test test/music_tags
xbmc/network/test test/network
xbmc/playlists/test test/playlists
xbmc/threads/test test/threads
xbmc/utils/test test/utils
xbmc/video/test test/video
Expand Down
2 changes: 1 addition & 1 deletion xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp
Expand Up @@ -340,7 +340,7 @@ void CGUIWindowMusicPlaylistEditor::OnLoadPlaylist()
share.strPath = "special://musicplaylists/";
if (find(shares.begin(), shares.end(), share) == shares.end())
shares.push_back(share);
if (CGUIDialogFileBrowser::ShowAndGetFile(shares, ".m3u|.pls|.b4s|.wpl", g_localizeStrings.Get(656), playlist))
if (CGUIDialogFileBrowser::ShowAndGetFile(shares, ".m3u|.pls|.b4s|.wpl|.xspf", g_localizeStrings.Get(656), playlist))
LoadPlaylist(playlist);
}

Expand Down
2 changes: 2 additions & 0 deletions xbmc/platform/darwin/ios/Info.plist.in
Expand Up @@ -320,6 +320,7 @@
<string>aif</string>
<string>aiff</string>
<string>wpl</string>
<string>xspf</string>

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

<string>ape</string>
<string>mac</string>
<string>mpc</string>
Expand Down Expand Up @@ -431,6 +432,7 @@
<string>rar</string>
<string>001</string>
<string>wpl</string>
<string>xspf</string>
<string>zip</string>
<string>vdr</string>
<string>dvr-ms</string>
Expand Down
2 changes: 2 additions & 0 deletions xbmc/playlists/CMakeLists.txt
Expand Up @@ -6,6 +6,7 @@ set(SOURCES PlayListB4S.cpp
PlayListURL.cpp
PlayListWPL.cpp
PlayListXML.cpp
PlayListXSPF.cpp
SmartPlayList.cpp
SmartPlaylistFileItemListModifier.cpp)

Expand All @@ -17,6 +18,7 @@ set(HEADERS PlayList.h
PlayListURL.h
PlayListWPL.h
PlayListXML.h
PlayListXSPF.h
SmartPlayList.h
SmartPlaylistFileItemListModifier.h)

Expand Down
11 changes: 9 additions & 2 deletions xbmc/playlists/PlayListFactory.cpp
Expand Up @@ -13,6 +13,7 @@
#include "PlayListWPL.h"
#include "PlayListURL.h"
#include "PlayListXML.h"
#include "PlayListXSPF.h"
#include "utils/URIUtils.h"
#include "utils/StringUtils.h"

Expand Down Expand Up @@ -58,6 +59,9 @@ CPlayList* CPlayListFactory::Create(const CFileItem& item)

if (strMimeType == "application/vnd.ms-wpl")
return new CPlayListWPL();

if (strMimeType == "application/xspf+xml")
return new CPlayListXSPF();
}

std::string path = item.GetDynPath();
Expand Down Expand Up @@ -89,6 +93,9 @@ CPlayList* CPlayListFactory::Create(const CFileItem& item)
if (extension == ".pxml")
return new CPlayListXML();

if (extension == ".xspf")
return new CPlayListXSPF();

return NULL;

}
Expand Down Expand Up @@ -125,12 +132,12 @@ bool CPlayListFactory::IsPlaylist(const CFileItem& item)
bool CPlayListFactory::IsPlaylist(const CURL& url)
{
return URIUtils::HasExtension(url,
".m3u|.b4s|.pls|.strm|.wpl|.asx|.ram|.url|.pxml");
".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");
".m3u|.b4s|.pls|.strm|.wpl|.asx|.ram|.url|.pxml|.xspf");
}

127 changes: 127 additions & 0 deletions xbmc/playlists/PlayListXSPF.cpp
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2018 Tyler Szabo
tylerszabo marked this conversation as resolved.
Show resolved Hide resolved
* 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 "utils/log.h"
#include "utils/URIUtils.h"
#include "utils/XBMCTinyXML.h"
#include "URL.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;
}

tylerszabo marked this conversation as resolved.
Show resolved Hide resolved
}

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 %s (%d, %d): %s", strFileName.c_str(), xmlDoc.ErrorRow(), xmlDoc.ErrorCol(), xmlDoc.ErrorDesc());
return false;
}

TiXmlElement* pPlaylist = xmlDoc.FirstChildElement(PLAYLIST_TAGNAME);
if (!pPlaylist)
{
CLog::Log(LOGERROR, "Error parsing XML file %s: missing root element %s", strFileName.c_str(), PLAYLIST_TAGNAME);
return false;
}

TiXmlElement* pTracklist = pPlaylist->FirstChildElement(TRACKLIST_TAGNAME);
if (!pTracklist)
{
CLog::Log(LOGERROR, "Error parsing XML file %s: missing element %s", strFileName.c_str(), 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
tylerszabo marked this conversation as resolved.
Show resolved Hide resolved
// 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())
{
#ifdef TARGET_WINDOWS
StringUtils::Replace(localpath, "/", "\\");
localpath = URIUtils::CanonicalizePath(localpath, '\\');
#else
localpath = URIUtils::CanonicalizePath(localpath, '/');
#endif
tylerszabo marked this conversation as resolved.
Show resolved Hide resolved

newItem->SetPath(localpath);
}
else
{
newItem->SetURL(uri);
}

Add(newItem);
}

pCurTrack = pCurTrack->NextSiblingElement(TRACK_TAGNAME);
}

return true;
}
24 changes: 24 additions & 0 deletions 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;
tylerszabo marked this conversation as resolved.
Show resolved Hide resolved
};
}
4 changes: 4 additions & 0 deletions xbmc/playlists/test/CMakeLists.txt
@@ -0,0 +1,4 @@
set(SOURCES TestPlayListFactory.cpp
TestPlayListXSPF.cpp)

core_add_test_library(playlists_test)
39 changes: 39 additions & 0 deletions xbmc/playlists/test/TestPlayListFactory.cpp
@@ -0,0 +1,39 @@
/*
* 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 "playlists/PlayListFactory.h"

#include "playlists/PlayList.h"
#include "playlists/PlayListXSPF.h"
#include "test/TestUtils.h"
#include "URL.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;
}
}
80 changes: 80 additions & 0 deletions xbmc/playlists/test/TestPlayListXSPF.cpp
@@ -0,0 +1,80 @@
/*
* 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 "playlists/PlayListXSPF.h"

#include "test/TestUtils.h"
#include "utils/URIUtils.h"
#include "FileItem.h"
#include "URL.h"

#include "gtest/gtest.h"

using namespace PLAYLIST;


tylerszabo marked this conversation as resolved.
Show resolved Hide resolved
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");
}