mirror of
https://github.com/projectM-visualizer/projectm.git
synced 2026-02-05 15:35:45 +00:00
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:
@ -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
159
src/playlist/Filter.cpp
Normal 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(¤tFilenameChar[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
53
src/playlist/Filter.hpp
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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>**‍/</strong>: Matches 0 or more directories.</li>
|
||||
* <li><strong>/‍**</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 "-/‍**" 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"
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
|
||||
180
tests/playlist/FilterTest.cpp
Normal file
180
tests/playlist/FilterTest.cpp
Normal 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"));
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
#include "PlaylistCWrapperMock.h"
|
||||
@ -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, ());
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user