diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a6e11412c..e64e1439b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,7 @@ set(CLEMENTINE-SOURCES shortcutsdialog.cpp albumcovermanager.cpp albumcoverloader.cpp + m3uparser.cpp ) # Header files that have Q_OBJECT in @@ -99,6 +100,7 @@ set(CLEMENTINE-MOC-HEADERS shortcutsdialog.h albumcovermanager.h albumcoverloader.h + m3uparser.h ) # UI files @@ -148,6 +150,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/qtsingleapplication") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/qxt") +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/gtest/include") set(EXECUTABLE_OUTPUT_PATH ..) add_library(clementine_lib diff --git a/src/m3uparser.cpp b/src/m3uparser.cpp new file mode 100644 index 000000000..8860dd6a8 --- /dev/null +++ b/src/m3uparser.cpp @@ -0,0 +1,91 @@ +#include "m3uparser.h" + +#include + +M3UParser::M3UParser(QIODevice* device, QDir directory, QObject* parent) + : QObject(parent), + device_(device), + type_(STANDARD), + directory_(directory) { +} + +const QList& M3UParser::Parse() { + QString line = QString::fromLatin1(device_->readLine()); + if (line == "#EXTM3U") { + // This is in extended M3U format. + type_ = EXTENDED; + line = QString::fromLatin1(device_->readLine()); + } + + do { + if (line.startsWith('#')) { + // Extended info or comment. + if (type_ == EXTENDED && line.startsWith("#EXT")) { + if (!ParseMetadata(line, ¤t_metadata_)) { + qWarning() << "Failed to parse metadata: " << line; + } + } + } else { + // Track location. + QUrl url; + if (!ParseTrackLocation(line, &url)) { + qWarning() << "Failed to parse location: " << line; + } + } + line = QString::fromLatin1(device_->readLine()); + } while (device_->canReadLine()); + + return songs_; +} + +bool M3UParser::ParseMetadata(QString line, M3UParser::Metadata* metadata) const { + // Extended info, eg. + // #EXTINF:123,Sample Artist - Sample title + QString info = line.section(':', 1); + QString l = info.section(',', 0, 0); + bool ok = false; + int length = l.toInt(&ok); + if (!ok) { + return false; + } + QString track_info = info.section(',', 1); + QStringList list = track_info.split('-'); + if (list.size() <= 1) { + return false; + } + metadata->artist = list[0].trimmed(); + metadata->title = list[1].trimmed(); + metadata->length = length; + return true; +} + +bool M3UParser::ParseTrackLocation(QString line, QUrl* url) const { + if (line.contains(QRegExp("^[a-z]+://"))) { + // Looks like a url. + QUrl temp(line); + if (temp.isValid()) { + *url = temp; + return true; + } else { + return false; + } + } + + // Should be a local path. + if (line.contains(QRegExp("^([a-zA-Z]:\\)|(/)"))) { + // Absolute path. + // Fix windows \. + QString proper_path = line.replace('\\', '/'); + if (!proper_path.startsWith('/')) { + proper_path.prepend("/"); + } + *url = "file://" + proper_path; + return true; + } else { + // Relative path. + QString proper_path = line.replace('\\', '/'); + QString absolute_path = directory_.absoluteFilePath(proper_path); + *url = "file://" + absolute_path; + return true; + } +} diff --git a/src/m3uparser.h b/src/m3uparser.h new file mode 100644 index 000000000..2bc315b76 --- /dev/null +++ b/src/m3uparser.h @@ -0,0 +1,49 @@ +#ifndef M3UPARSER_H +#define M3UPARSER_H + +#include +#include +#include + +#include "gtest/gtest_prod.h" + +#include "song.h" + +class QIODevice; + +class M3UParser : public QObject { + Q_OBJECT + public: + M3UParser(QIODevice* device, QDir directory = QDir(), QObject* parent = 0); + virtual ~M3UParser() {} + + const QList& Parse(); + + struct Metadata { + QString artist; + QString title; + int length; + }; + + private: + enum M3UType { + STANDARD = 0, + EXTENDED, // Includes extended info (track, artist, etc.) + LINK // Points to a directory. + }; + + bool ParseMetadata(QString line, Metadata* metadata) const; + bool ParseTrackLocation(QString line, QUrl* url) const; + + FRIEND_TEST(M3UParserTest, ParsesMetadata); + FRIEND_TEST(M3UParserTest, ParsesTrackLocation); + + QIODevice* device_; + M3UType type_; + QDir directory_; + Metadata current_metadata_; + + QList songs_; +}; + +#endif // M3UPARSER_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8023e00f1..e183dfb23 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,20 +12,27 @@ set(GTEST-SOURCES ../3rdparty/gtest/src/gtest-port.cc ../3rdparty/gtest/src/gtest-test-part.cc ../3rdparty/gtest/src/gtest-typed-test.cc + test_utils.cpp ) add_library(gtest ${GTEST-SOURCES}) - -set(SONGTEST-SOURCES - ../3rdparty/gtest/src/gtest_main.cc - ../src/song.cpp - song_test.cpp) - -add_executable(song_test ${SONGTEST-SOURCES}) -target_link_libraries(song_test clementine_lib gtest) - add_custom_target(test - ./song_test + echo "Running tests" WORKING_DIRECTORY ${CURRENT_BINARY_DIR} - DEPENDS song_test) + DEPENDS song_test m3uparser_test) + +# Given a file foo_test.cpp, creates a target foo_test and adds it to the test target. +macro(add_test_file test_source) + get_filename_component(TEST_NAME ${ARGV0} NAME_WE) + add_executable(${TEST_NAME} + ${ARGV0} + ../3rdparty/gtest/src/gtest_main.cc) + target_link_libraries(${TEST_NAME} clementine_lib gtest) + add_custom_command(TARGET test POST_BUILD + COMMAND ./${TEST_NAME}) +endmacro (add_test_file) + + +add_test_file(m3uparser_test.cpp) +add_test_file(song_test.cpp) diff --git a/tests/m3uparser_test.cpp b/tests/m3uparser_test.cpp new file mode 100644 index 000000000..496d10a9c --- /dev/null +++ b/tests/m3uparser_test.cpp @@ -0,0 +1,29 @@ +#include "gtest/gtest.h" +#include "test_utils.h" + +#include "m3uparser.h" + +class M3UParserTest : public ::testing::Test { + protected: + M3UParserTest() + : parser_(NULL) { + } + + M3UParser parser_; +}; + +TEST_F(M3UParserTest, ParsesMetadata) { + QString line("#EXTINF:123,Foo artist - Foo track"); + M3UParser::Metadata metadata; + ASSERT_TRUE(parser_.ParseMetadata(line, &metadata)); + EXPECT_EQ("Foo artist", metadata.artist.toStdString()); + EXPECT_EQ("Foo track", metadata.title.toStdString()); + EXPECT_EQ(123, metadata.length); +} + +TEST_F(M3UParserTest, ParsesTrackLocation) { + QString line("/foo/bar.mp3"); + QUrl url; + ASSERT_TRUE(parser_.ParseTrackLocation(line, &url)); + EXPECT_EQ(QUrl("file:///foo/bar.mp3"), url); +} diff --git a/tests/song_test.cpp b/tests/song_test.cpp index aba5e47e2..10a57d140 100644 --- a/tests/song_test.cpp +++ b/tests/song_test.cpp @@ -3,10 +3,7 @@ #include "gtest/gtest.h" -std::ostream& operator<<(std::ostream& stream, const QString& str) { - stream << str.toStdString(); - return stream; -} +#include "test_utils.h" namespace { diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp new file mode 100644 index 000000000..c8d3eaaab --- /dev/null +++ b/tests/test_utils.cpp @@ -0,0 +1,15 @@ +#include "test_utils.h" + +#include +#include + +std::ostream& operator<<(std::ostream& stream, const QString& str) { + stream << str.toStdString(); + return stream; +} + +std::ostream& operator <<(std::ostream& stream, const QUrl& url) { + stream << url.toString().toStdString(); + return stream; +} + diff --git a/tests/test_utils.h b/tests/test_utils.h new file mode 100644 index 000000000..fe4d69e78 --- /dev/null +++ b/tests/test_utils.h @@ -0,0 +1,12 @@ +#ifndef TEST_UTILS_H +#define TEST_UTILS_H + +#include + +class QString; +class QUrl; + +std::ostream& operator <<(std::ostream& stream, const QString& str); +std::ostream& operator <<(std::ostream& stream, const QUrl& url); + +#endif // TEST_UTILS_H