Add glob pattern filtering to playlist library.

Syntax is very similar to .gitignore glob syntax, with a few exceptions to simplify use.
This commit is contained in:
Kai Blaschke
2022-12-04 20:54:15 +01:00
parent a1ffd93e31
commit 9ea98bf01e
13 changed files with 681 additions and 13 deletions

View File

@ -3,6 +3,8 @@ if(NOT ENABLE_PLAYLIST)
endif()
add_library(projectM_playlist STATIC
Filter.cpp
Filter.hpp
Item.cpp
Item.hpp
Playlist.cpp

159
src/playlist/Filter.cpp Normal file
View File

@ -0,0 +1,159 @@
#include "Filter.hpp"
#include <cstring>
namespace ProjectM {
namespace Playlist {
auto Filter::List() const -> const std::vector<std::string>&
{
return m_filters;
}
void Filter::SetList(std::vector<std::string> filterList)
{
m_filters = std::move(filterList);
}
auto Filter::Passes(const std::string& filename) -> bool
{
for (const auto& filterExpression : m_filters)
{
if (!filterExpression.empty() && ApplyExpression(filename, filterExpression))
{
// Default action is "remove if filename matches".
return filterExpression.at(0) == '+';
}
}
return true;
}
auto Filter::ApplyExpression(const std::string& filename, const std::string& filterExpression) -> bool
{
// Implementation idea thanks to Robert van Engelen
// https://www.codeproject.com/Articles/5163931/Fast-String-Matching-with-Wildcards-Globs-and-Giti
if (filename.empty() || filterExpression.empty())
{
return false;
}
const auto* currentFilenameChar{filename.c_str()};
const auto* currentFilterChar{filterExpression.c_str()};
const char* previousFilenameChar{nullptr};
const char* previousFilterChar{nullptr};
bool inPathglob{false}; //!< True if the glob has a '**' pattern
auto isPathSep = [](const char* character) {
return *character == '/' || *character == '\\';
};
if (*currentFilterChar == '+' || *currentFilterChar == '-')
{
currentFilterChar++;
}
if (isPathSep(currentFilterChar))
{
while (*currentFilenameChar == '.' && isPathSep(&currentFilenameChar[1]))
{
currentFilenameChar += 2;
}
while (isPathSep(currentFilenameChar))
{
currentFilenameChar++;
}
currentFilterChar++;
}
else if (strchr(currentFilterChar, '/') == nullptr && strchr(currentFilterChar, '\\') == nullptr)
{
const auto *separatorUnix = strrchr(currentFilenameChar, '/');
const auto *separatorwindows = strrchr(currentFilenameChar, '\\');
if (separatorUnix != nullptr && separatorwindows != nullptr)
{
currentFilenameChar = std::min(separatorUnix, separatorwindows) + 1;
}
else if (separatorUnix != nullptr)
{
currentFilenameChar = separatorUnix + 1;
}
else if (separatorwindows != nullptr)
{
currentFilenameChar = separatorwindows + 1;
}
}
while (*currentFilenameChar != '\0')
{
switch (*currentFilterChar)
{
case '*':
previousFilenameChar = currentFilenameChar;
previousFilterChar = currentFilterChar;
inPathglob = false;
currentFilterChar++;
if (*currentFilterChar == '*')
{
currentFilterChar++;
if (*currentFilterChar == '\0')
{
return true;
}
if (!isPathSep(currentFilterChar))
{
return false;
}
inPathglob = true;
currentFilterChar++;
}
continue;
case '?':
if (isPathSep(currentFilenameChar))
{
break;
}
currentFilenameChar++;
currentFilterChar++;
default:
if (*currentFilterChar != *currentFilenameChar &&
!(isPathSep(currentFilterChar) && isPathSep(currentFilenameChar)))
{
break;
}
currentFilenameChar++;
currentFilterChar++;
continue;
}
if (previousFilterChar != nullptr && (inPathglob || !isPathSep(previousFilenameChar)))
{
currentFilenameChar = ++previousFilenameChar;
currentFilterChar = previousFilterChar;
continue;
}
return false;
}
while (*currentFilterChar == '*')
{
currentFilterChar++;
}
return *currentFilterChar == '\0';
}
} // namespace Playlist
} // namespace ProjectM

53
src/playlist/Filter.hpp Normal file
View File

@ -0,0 +1,53 @@
#pragma once
#include <string>
#include <vector>
namespace ProjectM {
namespace Playlist {
/**
* @brief Implements a simple filename globbing filter.
*
* See API docs of projectm_playlist_set_filter() in playlist.h for syntax details.
*/
class Filter
{
public:
/**
* @brief Returns the filter list.
* @return The current filter list.
*/
auto List() const -> const std::vector<std::string>&;
/**
* @brief Sets the filter list.
* @param filterList The new filter list.
*/
void SetList(std::vector<std::string> filterList);
/**
* @brief Applies the current filter list to the filename.
*
* This will apply all rules in order, and return true if the filename should be included
* in the playlist. If no rule matches or the filter list is empty, the filename will pass.
*
* @param filename The filename to check.
* @return True if the filename passes the filter, false if it should b skipped.
*/
auto Passes(const std::string& filename) ->bool;
private:
/**
* @brief Applies a single filter to the given filename.
* @param character The filename to check.
* @param filterExpression The filter expression. A leading + or - is ignored.
* @return True if the filter matches the filename, false otherwise.
*/
static auto ApplyExpression(const std::string& character, const std::string& filterExpression) -> bool;
std::vector<std::string> m_filters; //!< List of filters to apply.
};
} // namespace Playlist
} // namespace ProjectM

View File

@ -52,6 +52,11 @@ bool Playlist::AddItem(const std::string& filename, uint32_t index, bool allowDu
return false;
}
if (!m_filter.Passes(filename))
{
return false;
}
if (!allowDuplicates)
{
if (std::find(m_items.begin(), m_items.end(), filename) != m_items.end())
@ -301,6 +306,47 @@ auto Playlist::SetPresetIndex(size_t presetIndex) -> size_t
}
void Playlist::RemoveLastHistoryEntry()
{
if (!m_presetHistory.empty())
{
m_presetHistory.pop_back();
}
}
auto Playlist::Filter() -> class Filter&
{
return m_filter;
}
auto Playlist::ApplyFilter() -> size_t
{
size_t itemsRemoved{};
for (auto it = begin(m_items); it != end(m_items);)
{
if (!m_filter.Passes(it->Filename()))
{
it = m_items.erase(it);
itemsRemoved++;
}
else
{
++it;
}
}
if (itemsRemoved != 0)
{
m_presetHistory.clear();
}
return itemsRemoved;
}
void Playlist::AddCurrentPresetIndexToHistory()
{
// No duplicate entries.
@ -317,14 +363,5 @@ void Playlist::AddCurrentPresetIndexToHistory()
}
void Playlist::RemoveLastHistoryEntry()
{
if (!m_presetHistory.empty())
{
m_presetHistory.pop_back();
}
}
} // namespace Playlist
} // namespace ProjectM

View File

@ -1,5 +1,6 @@
#pragma once
#include "Filter.hpp"
#include "Item.hpp"
#include <cstdint>
@ -225,13 +226,29 @@ public:
*/
virtual void RemoveLastHistoryEntry();
/**
* @brief Returns the current playlist filter list.
* @return The filter list for the current playlist.
*/
virtual auto Filter() -> class Filter&;
/**
* @brief Applies the current filter list to the existing playlist.
*
* Note this function only removes items. Previously filtered items are not added again.
*
* @return The number of filtered (removed) items.
*/
virtual auto ApplyFilter() -> size_t;
private:
/**
* @brief Adds a preset to the history and trims the list if it gets too long.
*/
void AddCurrentPresetIndexToHistory();
std::vector<Item> m_items; //!< Items in the current playlist.
std::vector<Item> m_items; //!< All items in the current playlist.
class Filter m_filter; //!< Item filter.
bool m_shuffle{false}; //!< True if shuffle mode is enabled, false to play presets in order.
size_t m_currentPosition{0}; //!< Current playlist position.
std::list<size_t> m_presetHistory; //!< The playback history.

View File

@ -464,6 +464,7 @@ uint32_t projectm_playlist_play_previous(projectm_playlist_handle instance, bool
}
}
uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool hard_cut)
{
auto* playlist = playlist_handle_to_instance(instance);
@ -477,4 +478,51 @@ uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool har
{
return 0;
}
}
void projectm_playlist_set_filter(projectm_playlist_handle instance, const char** filter_list,
size_t count)
{
auto* playlist = playlist_handle_to_instance(instance);
std::vector<std::string> filterList;
for (size_t index = 0; index < count; index++)
{
if (filter_list[index] == nullptr)
{
continue;
}
filterList.emplace_back(filter_list[index]);
}
playlist->Filter().SetList(filterList);
}
auto projectm_playlist_get_filter(projectm_playlist_handle instance, size_t* count) -> char**
{
auto* playlist = playlist_handle_to_instance(instance);
const auto& filterList = playlist->Filter().List();
auto* array = new char* [filterList.size() + 1] {};
int index{0};
for (const auto& filter : filterList)
{
array[index] = new char[filter.length() + 1]{};
filter.copy(array[index], filter.length());
index++;
}
*count = filterList.size();
return array;
}
auto projectm_playlist_apply_filter(projectm_playlist_handle instance) -> size_t
{
auto* playlist = playlist_handle_to_instance(instance);
return playlist->ApplyFilter();
}

View File

@ -419,6 +419,69 @@ uint32_t projectm_playlist_play_previous(projectm_playlist_handle instance, bool
*/
uint32_t projectm_playlist_play_last(projectm_playlist_handle instance, bool hard_cut);
/**
* @brief Sets a new filter list.
*
* <p>Does not immediately apply the new list to an existing playlist, only newly added files
* will be affected. If you need to filter the existing playlist after calling this method,
* additionally call projectm_playlist_apply_filter() afterwards.</p>
*
* <p>The filter list consists of simple globbing expressions similar to the .gitignore syntax:</p>
*
* <ul>
* <li><strong>?</strong>: Matches any single character except /.</li>
* <li><strong>*</strong>: Matches 0 or more characters except /.</li>
* <li><strong>/</strong>: When used at the begin of a glob, matches if
* pathname has no path separator.</li>
* <li><strong>**&zwj;/</strong>: Matches 0 or more directories.</li>
* <li><strong>/&zwj;**</strong>: When at the end of the glob, matches everything after the /.</li>
* </ul>
*
* <p>In globbing expressions, \\ can be used as path separator instead of /. The backslash can't
* be used to escape globbing patterns, so matching literal * and ? in filenames is not possible.
* This is not a huge issue as Windows doesn't allow those characters in filenames and Milkdrop
* files originate from the Windows world. Character classes like "[0-9]" are also not supported to
* keep the syntax simple.</p>
*
* <p>Each line can be prefixed with either + or - to either include files matching the pattern
* or excluding them. Any other character is not interpreted as a prefix and the filter line is
* matching as an exclude filter. To match a literal + or - at the beginning, add the appropriate
* prefix in front. Empty filters never match anything, even if the filename is empty.</p>
*
* <p>The filter list is checked in order. The first pattern that matches the filename determines
* the filter result (include or exclude). If no pattern matches, the file is included. In the case
* that a default exclude action is required, add a "-/&zwj;**" filter at the end of the list.</p>
*
* @param instance The playlist manager instance.
* @param filter_list An array with filter strings.
* @param count The size of the filter array.
*/
void projectm_playlist_set_filter(projectm_playlist_handle instance, const char** filter_list,
size_t count);
/**
* @brief Returns the current filter list.
*
* Always call projectm_playlist_free_string_array() on the returned pointer if the data is
* no longer needed.
*
* @param instance The playlist manager instance.
* @param[out] count The size of the filter array.
* @return An array with filter strings.
*/
char** projectm_playlist_get_filter(projectm_playlist_handle instance, size_t* count);
/**
* @brief Applies the current filter list to the existing playlist.
*
* Note this function only removes items. Previously filtered items are not added again. If items
* were removed, the playback history is cleared.
*
* @param instance The playlist manager instance.
* @return The number of removed items.
*/
size_t projectm_playlist_apply_filter(projectm_playlist_handle instance);
#ifdef __cplusplus
}
// extern "C"

View File

@ -506,4 +506,66 @@ TEST(projectMPlaylistAPI, SetPresetSwitchFailedCallback)
.Times(1);
projectm_playlist_set_preset_switch_failed_event_callback(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), dummyCallback, dummyData);
}
TEST(projectMPlaylistAPI, SetFilter)
{
PlaylistCWrapperMock mockPlaylist;
ProjectM::Playlist::Filter filter;
EXPECT_CALL(mockPlaylist, Filter())
.Times(1)
.WillOnce(ReturnRef(filter));
const char firstFilter[]{"-/some/BadPreset.milk"};
const char secondFilter[]{"+/another/AwesomePreset.milk"};
const char thirdFilter[]{"-/unwanted/Preset.milk"};
const char* filterList[]{firstFilter, secondFilter, thirdFilter};
projectm_playlist_set_filter(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), filterList, 3);
const auto& internalFilterList = filter.List();
ASSERT_EQ(internalFilterList.size(), 3);
EXPECT_EQ(internalFilterList.at(0), "-/some/BadPreset.milk");
EXPECT_EQ(internalFilterList.at(1), "+/another/AwesomePreset.milk");
EXPECT_EQ(internalFilterList.at(2), "-/unwanted/Preset.milk");
}
TEST(projectMPlaylistAPI, GetFilter)
{
PlaylistCWrapperMock mockPlaylist;
ProjectM::Playlist::Filter filter;
filter.SetList({"-/some/BadPreset.milk",
"+/another/AwesomePreset.milk",
"-/unwanted/Preset.milk"});
EXPECT_CALL(mockPlaylist, Filter())
.Times(1)
.WillOnce(ReturnRef(filter));
size_t count{};
auto filterList = projectm_playlist_get_filter(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist), &count);
ASSERT_EQ(count, 3);
ASSERT_NE(filterList, nullptr);
EXPECT_STREQ(filterList[0], "-/some/BadPreset.milk");
EXPECT_STREQ(filterList[1], "+/another/AwesomePreset.milk");
EXPECT_STREQ(filterList[2], "-/unwanted/Preset.milk");
projectm_playlist_free_string_array(filterList);
}
TEST(projectMPlaylistAPI, ApplyFilter)
{
PlaylistCWrapperMock mockPlaylist;
EXPECT_CALL(mockPlaylist, ApplyFilter())
.Times(1)
.WillOnce(Return(5));
EXPECT_EQ(projectm_playlist_apply_filter(reinterpret_cast<projectm_playlist_handle>(&mockPlaylist)), 5);
}

View File

@ -7,11 +7,10 @@ find_package(GTest 1.10 REQUIRED NO_MODULE)
add_executable(projectM-playlist-unittest
APITest.cpp
ItemTest.cpp
PlaylistCWrapperMock.cpp
PlaylistCWrapperMock.h
PlaylistTest.cpp
ProjectMAPIMocks.cpp
)
FilterTest.cpp)
target_compile_definitions(projectM-playlist-unittest
PRIVATE

View File

@ -0,0 +1,180 @@
#include <Filter.hpp>
#include <gtest/gtest.h>
TEST(projectMPlaylistFilter, List)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-TestString.milk",
"+AnotherTestString*"});
const auto& filters = filter.List();
ASSERT_EQ(filters.size(), 2);
EXPECT_EQ(filters.at(0), "-TestString.milk");
EXPECT_EQ(filters.at(1), "+AnotherTestString*");
}
TEST(projectMPlaylistFilter, ExactMatchExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-TestString.milk"});
EXPECT_FALSE(filter.Passes("TestString.milk"));
EXPECT_TRUE(filter.Passes("Teststring.milk"));
}
TEST(projectMPlaylistFilter, ExactMatchExcludePath)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/TestString.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestString.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Teststring.milk"));
}
TEST(projectMPlaylistFilter, SingleCharacterExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/TestStr?ng.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestString.milk"));
EXPECT_FALSE(filter.Passes("/path/to/TestStrung.milk"));
EXPECT_TRUE(filter.Passes("/path/to/TestStr/ng.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Teststring.milk"));
}
TEST(projectMPlaylistFilter, MultiCharacterExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/Test*.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestString.milk"));
EXPECT_FALSE(filter.Passes("/path/to/TestFile.milk"));
EXPECT_FALSE(filter.Passes("/path/to/TestALotOfAdditional.Characters.milk"));
EXPECT_FALSE(filter.Passes("/path/to/Test.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Test/String.milk"));
}
TEST(projectMPlaylistFilter, MultiCharacterExcludeExamples)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-a"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("x/a"));
EXPECT_FALSE(filter.Passes("x/y/a"));
EXPECT_TRUE(filter.Passes("b"));
EXPECT_TRUE(filter.Passes("x/b"));
EXPECT_TRUE(filter.Passes("a/a/b"));
filter.SetList({"-/*"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("b"));
EXPECT_TRUE(filter.Passes("x/a"));
EXPECT_TRUE(filter.Passes("x/b"));
EXPECT_TRUE(filter.Passes("x/y/a"));
filter.SetList({"-/a"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("/a"));
EXPECT_FALSE(filter.Passes("./a"));
EXPECT_TRUE(filter.Passes("x/a"));
EXPECT_TRUE(filter.Passes("x/y/a"));
}
TEST(projectMPlaylistFilter, PathGlobExclude)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-**/Test.milk"});
EXPECT_FALSE(filter.Passes("/path/to/Test.milk"));
EXPECT_FALSE(filter.Passes("/path/Test.milk"));
EXPECT_FALSE(filter.Passes("Test.milk"));
EXPECT_FALSE(filter.Passes("\\path\\to\\path\\to\\path\\to/path/to/path/to/Test.milk"));
EXPECT_TRUE(filter.Passes("/path/to/Test/.milk"));
}
TEST(projectMPlaylistFilter, PathGlobExcludeExamples)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-**/a"});
EXPECT_FALSE(filter.Passes("a"));
EXPECT_FALSE(filter.Passes("x/a"));
EXPECT_FALSE(filter.Passes("x/y/a"));
EXPECT_TRUE(filter.Passes("b"));
EXPECT_TRUE(filter.Passes("x/b"));
filter.SetList({"-a/**/b"});
EXPECT_FALSE(filter.Passes("a/b"));
EXPECT_FALSE(filter.Passes("a/x/b"));
EXPECT_FALSE(filter.Passes("a/x/y/b"));
EXPECT_TRUE(filter.Passes("x/a/b"));
EXPECT_TRUE(filter.Passes("a/b/x"));
filter.SetList({"-a/**"});
EXPECT_FALSE(filter.Passes("a/x"));
EXPECT_FALSE(filter.Passes("a/y"));
EXPECT_FALSE(filter.Passes("a/x/y"));
EXPECT_TRUE(filter.Passes("a"));
EXPECT_TRUE(filter.Passes("b/x"));
}
TEST(projectMPlaylistFilter, LargeGlobs)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a"});
EXPECT_FALSE(filter.Passes("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
EXPECT_TRUE(filter.Passes("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
filter.SetList({"-/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a"});
EXPECT_FALSE(filter.Passes("/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a"));
EXPECT_TRUE(filter.Passes("/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b/b"));
}
TEST(projectMPlaylistFilter, MultipleFilters)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/path/to/Test*.milk",
"/path/to/another\\Test*.milk",
"+/path/to/yet/another\\Test*.milk",
"-Test*.milk"});
EXPECT_FALSE(filter.Passes("/path/to/TestSome.milk"));
EXPECT_FALSE(filter.Passes("/path/to/another/TestSome.milk"));
EXPECT_TRUE(filter.Passes("\\path\\to\\yet\\another\\TestCase.milk"));
EXPECT_FALSE(filter.Passes("/path/of/my/TestPreset.milk"));
EXPECT_TRUE(filter.Passes("/another/something/completely/different"));
}
TEST(projectMPlaylistFilter, MatchEverything)
{
ProjectM::Playlist::Filter filter;
filter.SetList({"-/**"});
EXPECT_FALSE(filter.Passes("/path/to/TestSome.milk"));
EXPECT_FALSE(filter.Passes("/path/to/another/TestSome.milk"));
EXPECT_FALSE(filter.Passes("\\path\\to\\yet\\another\\TestCase.milk"));
EXPECT_FALSE(filter.Passes("/path/of/my/TestPreset.milk"));
EXPECT_FALSE(filter.Passes("/another/something/completely/different"));
}

View File

@ -1 +0,0 @@
#include "PlaylistCWrapperMock.h"

View File

@ -33,4 +33,6 @@ public:
MOCK_METHOD(void, PlayPresetIndex, (size_t, bool, bool) );
MOCK_METHOD(void, RemoveLastHistoryEntry, ());
MOCK_METHOD(void, SetPresetSwitchFailedCallback, (projectm_playlist_preset_switch_failed_event, void*));
MOCK_METHOD(class ProjectM::Playlist::Filter&, Filter, ());
MOCK_METHOD(size_t, ApplyFilter, ());
};

View File

@ -627,3 +627,50 @@ TEST(projectMPlaylistPlaylist, RemoveLastHistoryEntry)
EXPECT_EQ(playlist.LastPresetIndex(), 0);
}
TEST(projectMPlaylistPlaylist, AddItemWithFilter)
{
Playlist playlist;
playlist.Filter().SetList({"-/**/Preset*.milk"});
EXPECT_FALSE(playlist.AddItem("/some/PresetZ.milk", Playlist::InsertAtEnd, false));
EXPECT_FALSE(playlist.AddItem("/some/PresetA.milk", Playlist::InsertAtEnd, false));
EXPECT_FALSE(playlist.AddItem("/some/other/PresetC.milk", Playlist::InsertAtEnd, false));
EXPECT_TRUE(playlist.AddItem("/some/MyFavorite.milk", Playlist::InsertAtEnd, false));
ASSERT_EQ(playlist.Size(), 1);
}
TEST(projectMPlaylistPlaylist, AddPathWithFilter)
{
Playlist playlist;
playlist.Filter().SetList({"-**/presets/Test_*.milk"});
EXPECT_EQ(playlist.AddPath(PROJECTM_PLAYLIST_TEST_DATA_DIR "/presets", 0, true, false), 1);
ASSERT_EQ(playlist.Size(), 1);
}
TEST(projectMPlaylistPlaylist, ApplyFilter)
{
Playlist playlist;
// Remove Test_A on load
playlist.Filter().SetList({"-Test_A.milk"});
EXPECT_EQ(playlist.AddPath(PROJECTM_PLAYLIST_TEST_DATA_DIR "/presets", 0, true, false), 3);
ASSERT_EQ(playlist.Size(), 3);
// Apply new filter that only removes Test_B
playlist.Filter().SetList({"-Test_B.milk"});
EXPECT_EQ(playlist.ApplyFilter(), 1);
// Test_A will not reappear.
ASSERT_EQ(playlist.Size(), 2);
}