diff options
author | Max Kellermann <max@musicpd.org> | 2018-08-02 13:45:43 +0200 |
---|---|---|
committer | Max Kellermann <max@musicpd.org> | 2018-08-02 13:51:18 +0200 |
commit | 90201e997085f04f5ac28333d2fbc70fbd6809f0 (patch) | |
tree | 932ba5f3618bc1efda878164e33bd3e6fd2f4d74 /src/song | |
parent | a31da51fd9bd215645b9cabce5fefa449ca14c90 (diff) |
DetachedSong, db/LightSong, SongFilter: move to src/song/
Diffstat (limited to 'src/song')
-rw-r--r-- | src/song/DetachedSong.cxx | 79 | ||||
-rw-r--r-- | src/song/DetachedSong.hxx | 248 | ||||
-rw-r--r-- | src/song/Filter.cxx | 539 | ||||
-rw-r--r-- | src/song/Filter.hxx | 301 | ||||
-rw-r--r-- | src/song/LightSong.cxx | 35 | ||||
-rw-r--r-- | src/song/LightSong.hxx | 106 |
6 files changed, 1308 insertions, 0 deletions
diff --git a/src/song/DetachedSong.cxx b/src/song/DetachedSong.cxx new file mode 100644 index 000000000..cb0eaf03a --- /dev/null +++ b/src/song/DetachedSong.cxx @@ -0,0 +1,79 @@ +/* + * Copyright 2003-2017 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "song/DetachedSong.hxx" +#include "song/LightSong.hxx" +#include "util/UriUtil.hxx" +#include "fs/Traits.hxx" + +DetachedSong::DetachedSong(const LightSong &other) + :uri(other.GetURI()), + real_uri(other.real_uri != nullptr ? other.real_uri : ""), + tag(other.tag), + mtime(other.mtime), + start_time(other.start_time), + end_time(other.end_time) {} + +DetachedSong::operator LightSong() const noexcept +{ + LightSong result(uri.c_str(), tag); + result.directory = nullptr; + result.real_uri = real_uri.empty() ? nullptr : real_uri.c_str(); + result.mtime = mtime; + result.start_time = start_time; + result.end_time = end_time; + return result; +} + +bool +DetachedSong::IsRemote() const noexcept +{ + return uri_has_scheme(GetRealURI()); +} + +bool +DetachedSong::IsAbsoluteFile() const noexcept +{ + return PathTraitsUTF8::IsAbsolute(GetRealURI()); +} + +bool +DetachedSong::IsInDatabase() const noexcept +{ + /* here, we use GetURI() and not GetRealURI() because + GetRealURI() is never relative */ + + const char *_uri = GetURI(); + return !uri_has_scheme(_uri) && !PathTraitsUTF8::IsAbsolute(_uri); +} + +SignedSongTime +DetachedSong::GetDuration() const noexcept +{ + SongTime a = start_time, b = end_time; + if (!b.IsPositive()) { + if (tag.duration.IsNegative()) + return tag.duration; + + b = SongTime(tag.duration); + } + + return SignedSongTime(b - a); +} diff --git a/src/song/DetachedSong.hxx b/src/song/DetachedSong.hxx new file mode 100644 index 000000000..a161ad427 --- /dev/null +++ b/src/song/DetachedSong.hxx @@ -0,0 +1,248 @@ +/* + * Copyright 2003-2017 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DETACHED_SONG_HXX +#define MPD_DETACHED_SONG_HXX + +#include "check.h" +#include "tag/Tag.hxx" +#include "Chrono.hxx" +#include "Compiler.h" + +#include <chrono> +#include <string> +#include <utility> + +struct LightSong; +class Storage; +class Path; + +class DetachedSong { + friend DetachedSong DatabaseDetachSong(const Storage &db, + const LightSong &song); + + /** + * An UTF-8-encoded URI referring to the song file. This can + * be one of: + * + * - an absolute URL with a scheme + * (e.g. "http://example.com/foo.mp3") + * + * - an absolute file name + * + * - a file name relative to the music directory + */ + std::string uri; + + /** + * The "real" URI, the one to be used for opening the + * resource. If this attribute is empty, then #uri shall be + * used. + * + * This attribute is used for songs from the database which + * have a relative URI. + */ + std::string real_uri; + + Tag tag; + + /** + * The time stamp of the last file modification. A negative + * value means that this is unknown/unavailable. + */ + std::chrono::system_clock::time_point mtime = + std::chrono::system_clock::time_point::min(); + + /** + * Start of this sub-song within the file. + */ + SongTime start_time = SongTime::zero(); + + /** + * End of this sub-song within the file. + * Unused if zero. + */ + SongTime end_time = SongTime::zero(); + +public: + explicit DetachedSong(const char *_uri) + :uri(_uri) {} + + explicit DetachedSong(const std::string &_uri) + :uri(_uri) {} + + explicit DetachedSong(std::string &&_uri) + :uri(std::move(_uri)) {} + + template<typename U> + DetachedSong(U &&_uri, Tag &&_tag) + :uri(std::forward<U>(_uri)), + tag(std::move(_tag)) {} + + /** + * Copy data from a #LightSong instance. Usually, you should + * call DatabaseDetachSong() instead, which initializes + * #real_uri properly using Storage::MapUTF8(). + */ + explicit DetachedSong(const LightSong &other); + + gcc_noinline + ~DetachedSong() = default; + + /* these are declared because the user-defined destructor + above prevents them from being generated implicitly */ + explicit DetachedSong(const DetachedSong &) = default; + DetachedSong(DetachedSong &&) = default; + DetachedSong &operator=(DetachedSong &&) = default; + + gcc_pure + explicit operator LightSong() const noexcept; + + gcc_pure + const char *GetURI() const noexcept { + return uri.c_str(); + } + + template<typename T> + void SetURI(T &&_uri) { + uri = std::forward<T>(_uri); + } + + /** + * Does this object have a "real" URI different from the + * displayed URI? + */ + gcc_pure + bool HasRealURI() const noexcept { + return !real_uri.empty(); + } + + /** + * Returns "real" URI (#real_uri) and falls back to just + * GetURI(). + */ + gcc_pure + const char *GetRealURI() const noexcept { + return (HasRealURI() ? real_uri : uri).c_str(); + } + + template<typename T> + void SetRealURI(T &&_uri) { + real_uri = std::forward<T>(_uri); + } + + /** + * Returns true if both objects refer to the same physical + * song. + */ + gcc_pure + bool IsSame(const DetachedSong &other) const noexcept { + return uri == other.uri && + start_time == other.start_time && + end_time == other.end_time; + } + + gcc_pure gcc_nonnull_all + bool IsURI(const char *other_uri) const noexcept { + return uri == other_uri; + } + + gcc_pure + bool IsRemote() const noexcept; + + gcc_pure + bool IsFile() const noexcept { + return !IsRemote(); + } + + gcc_pure + bool IsAbsoluteFile() const noexcept; + + gcc_pure + bool IsInDatabase() const noexcept; + + const Tag &GetTag() const noexcept { + return tag; + } + + Tag &WritableTag() noexcept { + return tag; + } + + void SetTag(const Tag &_tag) { + tag = Tag(_tag); + } + + void SetTag(Tag &&_tag) { + tag = std::move(_tag); + } + + void MoveTagFrom(DetachedSong &&other) { + tag = std::move(other.tag); + } + + /** + * Similar to the MoveTagFrom(), but move only the #TagItem + * array. + */ + void MoveTagItemsFrom(DetachedSong &&other) { + tag.MoveItemsFrom(std::move(other.tag)); + } + + std::chrono::system_clock::time_point GetLastModified() const { + return mtime; + } + + void SetLastModified(std::chrono::system_clock::time_point _value) { + mtime = _value; + } + + SongTime GetStartTime() const { + return start_time; + } + + void SetStartTime(SongTime _value) { + start_time = _value; + } + + SongTime GetEndTime() const { + return end_time; + } + + void SetEndTime(SongTime _value) { + end_time = _value; + } + + gcc_pure + SignedSongTime GetDuration() const noexcept; + + /** + * Update the #tag and #mtime. + * + * @return true on success + */ + bool Update() noexcept; + + /** + * Load #tag and #mtime from a local file. + */ + bool LoadFile(Path path) noexcept; +}; + +#endif diff --git a/src/song/Filter.cxx b/src/song/Filter.cxx new file mode 100644 index 000000000..6259cefae --- /dev/null +++ b/src/song/Filter.cxx @@ -0,0 +1,539 @@ +/* + * Copyright 2003-2018 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "Filter.hxx" +#include "LightSong.hxx" +#include "tag/ParseName.hxx" +#include "tag/Tag.hxx" +#include "util/CharUtil.hxx" +#include "util/ChronoUtil.hxx" +#include "util/ConstBuffer.hxx" +#include "util/RuntimeError.hxx" +#include "util/StringAPI.hxx" +#include "util/StringCompare.hxx" +#include "util/StringStrip.hxx" +#include "util/StringView.hxx" +#include "util/ASCII.hxx" +#include "util/TimeISO8601.hxx" +#include "util/UriUtil.hxx" +#include "lib/icu/CaseFold.hxx" + +#include <exception> + +#include <assert.h> +#include <stdlib.h> + +#define LOCATE_TAG_FILE_KEY "file" +#define LOCATE_TAG_FILE_KEY_OLD "filename" +#define LOCATE_TAG_ANY_KEY "any" + +/** + * Limit the search to files within the given directory. + */ +#define LOCATE_TAG_BASE_TYPE (TAG_NUM_OF_ITEM_TYPES + 1) +#define LOCATE_TAG_MODIFIED_SINCE (TAG_NUM_OF_ITEM_TYPES + 2) +#define LOCATE_TAG_FILE_TYPE TAG_NUM_OF_ITEM_TYPES+10 +#define LOCATE_TAG_ANY_TYPE TAG_NUM_OF_ITEM_TYPES+20 + +/** + * @return #TAG_NUM_OF_ITEM_TYPES on error + */ +gcc_pure +static unsigned +locate_parse_type(const char *str) noexcept +{ + if (StringEqualsCaseASCII(str, LOCATE_TAG_FILE_KEY) || + StringEqualsCaseASCII(str, LOCATE_TAG_FILE_KEY_OLD)) + return LOCATE_TAG_FILE_TYPE; + + if (StringEqualsCaseASCII(str, LOCATE_TAG_ANY_KEY)) + return LOCATE_TAG_ANY_TYPE; + + if (strcmp(str, "base") == 0) + return LOCATE_TAG_BASE_TYPE; + + if (strcmp(str, "modified-since") == 0) + return LOCATE_TAG_MODIFIED_SINCE; + + return tag_name_parse_i(str); +} + +bool +StringFilter::Match(const char *s) const noexcept +{ +#if !CLANG_CHECK_VERSION(3,6) + /* disabled on clang due to -Wtautological-pointer-compare */ + assert(s != nullptr); +#endif + + if (fold_case) { + return fold_case.IsIn(s); + } else { + return StringIsEqual(s, value.c_str()); + } +} + +std::string +UriSongFilter::ToExpression() const noexcept +{ + return std::string("(" LOCATE_TAG_FILE_KEY " ") + (negated ? "!=" : "==") + " \"" + filter.GetValue() + "\")"; +} + +bool +UriSongFilter::Match(const LightSong &song) const noexcept +{ + return filter.Match(song.GetURI().c_str()) != negated; +} + +std::string +BaseSongFilter::ToExpression() const noexcept +{ + return "(base \"" + value + "\")"; +} + +bool +BaseSongFilter::Match(const LightSong &song) const noexcept +{ + return uri_is_child_or_same(value.c_str(), song.GetURI().c_str()); +} + +std::string +TagSongFilter::ToExpression() const noexcept +{ + const char *name = type == TAG_NUM_OF_ITEM_TYPES + ? LOCATE_TAG_ANY_KEY + : tag_item_names[type]; + + return std::string("(") + name + " " + (negated ? "!=" : "==") + " \"" + filter.GetValue() + "\")"; +} + +bool +TagSongFilter::MatchNN(const TagItem &item) const noexcept +{ + return (type == TAG_NUM_OF_ITEM_TYPES || item.type == type) && + filter.Match(item.value); +} + +bool +TagSongFilter::MatchNN(const Tag &tag) const noexcept +{ + bool visited_types[TAG_NUM_OF_ITEM_TYPES]; + std::fill_n(visited_types, size_t(TAG_NUM_OF_ITEM_TYPES), false); + + for (const auto &i : tag) { + visited_types[i.type] = true; + + if (MatchNN(i)) + return true; + } + + if (type < TAG_NUM_OF_ITEM_TYPES && !visited_types[type]) { + /* If the search critieron was not visited during the + sweep through the song's tag, it means this field + is absent from the tag or empty. Thus, if the + searched string is also empty + then it's a match as well and we should return + true. */ + if (filter.empty()) + return true; + + if (type == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) { + /* if we're looking for "album artist", but + only "artist" exists, use that */ + for (const auto &item : tag) + if (item.type == TAG_ARTIST && + filter.Match(item.value)) + return true; + } + } + + return false; +} + +bool +TagSongFilter::Match(const LightSong &song) const noexcept +{ + return MatchNN(song.tag) != negated; +} + +std::string +ModifiedSinceSongFilter::ToExpression() const noexcept +{ + return std::string("(modified-since \"") + FormatISO8601(value).c_str() + "\")"; +} + +bool +ModifiedSinceSongFilter::Match(const LightSong &song) const noexcept +{ + return song.mtime >= value; +} + +ISongFilterPtr +AndSongFilter::Clone() const noexcept +{ + auto result = std::make_unique<AndSongFilter>(); + + for (const auto &i : items) + result->items.emplace_back(i->Clone()); + + return result; +} + +std::string +AndSongFilter::ToExpression() const noexcept +{ + auto i = items.begin(); + const auto end = items.end(); + + if (std::next(i) == end) + return (*i)->ToExpression(); + + std::string e("("); + e += (*i)->ToExpression(); + + for (++i; i != end; ++i) { + e += " AND "; + e += (*i)->ToExpression(); + } + + e.push_back(')'); + return e; +} + +bool +AndSongFilter::Match(const LightSong &song) const noexcept +{ + for (const auto &i : items) + if (!i->Match(song)) + return false; + + return true; +} + +SongFilter::SongFilter(TagType tag, const char *value, bool fold_case) +{ + and_filter.AddItem(std::make_unique<TagSongFilter>(tag, value, + fold_case, false)); +} + +SongFilter::~SongFilter() +{ + /* this destructor exists here just so it won't get inlined */ +} + +std::string +SongFilter::ToExpression() const noexcept +{ + return and_filter.ToExpression(); +} + +static std::chrono::system_clock::time_point +ParseTimeStamp(const char *s) +{ + assert(s != nullptr); + + char *endptr; + unsigned long long value = strtoull(s, &endptr, 10); + if (*endptr == 0 && endptr > s) + /* it's an integral UNIX time stamp */ + return std::chrono::system_clock::from_time_t((time_t)value); + + /* try ISO 8601 */ + return ParseISO8601(s); +} + +static constexpr bool +IsTagNameChar(char ch) noexcept +{ + return IsAlphaASCII(ch) || ch == '_' || ch == '-'; +} + +static const char * +FirstNonTagNameChar(const char *s) noexcept +{ + while (IsTagNameChar(*s)) + ++s; + return s; +} + +static auto +ExpectWord(const char *&s) +{ + const char *begin = s; + const char *end = FirstNonTagNameChar(s); + if (end == s) + throw std::runtime_error("Word expected"); + + s = StripLeft(end); + return std::string(begin, end); +} + +static auto +ExpectFilterType(const char *&s) +{ + const auto name = ExpectWord(s); + + const auto type = locate_parse_type(name.c_str()); + if (type == TAG_NUM_OF_ITEM_TYPES) + throw FormatRuntimeError("Unknown filter type: %s", + name.c_str()); + + return type; +} + +static constexpr bool +IsQuote(char ch) noexcept +{ + return ch == '"' || ch == '\''; +} + +static std::string +ExpectQuoted(const char *&s) +{ + const char quote = *s++; + if (!IsQuote(quote)) + throw std::runtime_error("Quoted string expected"); + + const char *begin = s; + const char *end = strchr(s, quote); + if (end == nullptr) + throw std::runtime_error("Closing quote not found"); + + s = StripLeft(end + 1); + return {begin, end}; +} + +ISongFilterPtr +SongFilter::ParseExpression(const char *&s, bool fold_case) +{ + assert(*s == '('); + + s = StripLeft(s + 1); + + if (*s == '(') { + auto first = ParseExpression(s, fold_case); + if (*s == ')') { + ++s; + return first; + } + + if (ExpectWord(s) != "AND") + throw std::runtime_error("'AND' expected"); + + auto and_filter = std::make_unique<AndSongFilter>(); + and_filter->AddItem(std::move(first)); + + while (true) { + and_filter->AddItem(ParseExpression(s, fold_case)); + + if (*s == ')') { + ++s; + return and_filter; + } + + if (ExpectWord(s) != "AND") + throw std::runtime_error("'AND' expected"); + } + } + + auto type = ExpectFilterType(s); + + if (type == LOCATE_TAG_MODIFIED_SINCE) { + const auto value_s = ExpectQuoted(s); + if (*s != ')') + throw std::runtime_error("')' expected"); + s = StripLeft(s + 1); + return std::make_unique<ModifiedSinceSongFilter>(ParseTimeStamp(value_s.c_str())); + } else if (type == LOCATE_TAG_BASE_TYPE) { + auto value = ExpectQuoted(s); + if (*s != ')') + throw std::runtime_error("')' expected"); + s = StripLeft(s + 1); + + return std::make_unique<BaseSongFilter>(std::move(value)); + } else { + bool negated = false; + if (s[0] == '!' && s[1] == '=') + negated = true; + else if (s[0] != '=' || s[1] != '=') + throw std::runtime_error("'==' or '!=' expected"); + + s = StripLeft(s + 2); + auto value = ExpectQuoted(s); + if (*s != ')') + throw std::runtime_error("')' expected"); + + s = StripLeft(s + 1); + + if (type == LOCATE_TAG_ANY_TYPE) + type = TAG_NUM_OF_ITEM_TYPES; + + if (type == LOCATE_TAG_FILE_TYPE) + return std::make_unique<UriSongFilter>(std::move(value), + fold_case, + negated); + + return std::make_unique<TagSongFilter>(TagType(type), + std::move(value), + fold_case, negated); + } +} + +void +SongFilter::Parse(const char *tag_string, const char *value, bool fold_case) +{ + unsigned tag = locate_parse_type(tag_string); + + switch (tag) { + case TAG_NUM_OF_ITEM_TYPES: + throw std::runtime_error("Unknown filter type"); + + case LOCATE_TAG_BASE_TYPE: + if (!uri_safe_local(value)) + throw std::runtime_error("Bad URI"); + + and_filter.AddItem(std::make_unique<BaseSongFilter>(value)); + break; + + case LOCATE_TAG_MODIFIED_SINCE: + and_filter.AddItem(std::make_unique<ModifiedSinceSongFilter>(ParseTimeStamp(value))); + break; + + case LOCATE_TAG_FILE_TYPE: + and_filter.AddItem(std::make_unique<UriSongFilter>(value, + fold_case, + false)); + break; + + default: + if (tag == LOCATE_TAG_ANY_TYPE) + tag = TAG_NUM_OF_ITEM_TYPES; + + and_filter.AddItem(std::make_unique<TagSongFilter>(TagType(tag), + value, + fold_case, + false)); + break; + } +} + +void +SongFilter::Parse(ConstBuffer<const char *> args, bool fold_case) +{ + if (args.empty()) + throw std::runtime_error("Incorrect number of filter arguments"); + + do { + if (*args.front() == '(') { + const char *s = args.shift(); + const char *end = s; + auto f = ParseExpression(end, fold_case); + if (*end != 0) + throw std::runtime_error("Unparsed garbage after expression"); + + and_filter.AddItem(std::move(f)); + continue; + } + + if (args.size < 2) + throw std::runtime_error("Incorrect number of filter arguments"); + + const char *tag = args.shift(); + const char *value = args.shift(); + Parse(tag, value, fold_case); + } while (!args.empty()); +} + +bool +SongFilter::Match(const LightSong &song) const noexcept +{ + return and_filter.Match(song); +} + +bool +SongFilter::HasFoldCase() const noexcept +{ + for (const auto &i : and_filter.GetItems()) { + if (auto t = dynamic_cast<const TagSongFilter *>(i.get())) { + if (t->GetFoldCase()) + return true; + } else if (auto u = dynamic_cast<const UriSongFilter *>(i.get())) { + if (u->GetFoldCase()) + return true; + } + } + + return false; +} + +bool +SongFilter::HasOtherThanBase() const noexcept +{ + for (const auto &i : and_filter.GetItems()) { + const auto *f = dynamic_cast<const BaseSongFilter *>(i.get()); + if (f == nullptr) + return true; + } + + return false; +} + +const char * +SongFilter::GetBase() const noexcept +{ + for (const auto &i : and_filter.GetItems()) { + const auto *f = dynamic_cast<const BaseSongFilter *>(i.get()); + if (f != nullptr) + return f->GetValue(); + } + + return nullptr; +} + +SongFilter +SongFilter::WithoutBasePrefix(const char *_prefix) const noexcept +{ + const StringView prefix(_prefix); + SongFilter result; + + for (const auto &i : and_filter.GetItems()) { + const auto *f = dynamic_cast<const BaseSongFilter *>(i.get()); + if (f != nullptr) { + const char *s = StringAfterPrefix(f->GetValue(), prefix); + if (s != nullptr) { + if (*s == 0) + continue; + + if (*s == '/') { + ++s; + + if (*s != 0) + result.and_filter.AddItem(std::make_unique<BaseSongFilter>(s)); + + continue; + } + } + } + + result.and_filter.AddItem(i->Clone()); + } + + return result; +} diff --git a/src/song/Filter.hxx b/src/song/Filter.hxx new file mode 100644 index 000000000..b627e4b38 --- /dev/null +++ b/src/song/Filter.hxx @@ -0,0 +1,301 @@ +/* + * Copyright 2003-2018 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_SONG_FILTER_HXX +#define MPD_SONG_FILTER_HXX + +#include "lib/icu/Compare.hxx" +#include "Compiler.h" + +#include <memory> +#include <string> +#include <list> +#include <chrono> + +#include <stdint.h> + +/** + * Special value for the db_selection_print() sort parameter. + */ +#define SORT_TAG_LAST_MODIFIED (TAG_NUM_OF_ITEM_TYPES + 3) + +template<typename T> struct ConstBuffer; +enum TagType : uint8_t; +struct Tag; +struct TagItem; +struct LightSong; +class ISongFilter; +using ISongFilterPtr = std::unique_ptr<ISongFilter>; + +class ISongFilter { +public: + virtual ~ISongFilter() noexcept {} + + virtual ISongFilterPtr Clone() const noexcept = 0; + + /** + * Convert this object into an "expression". This is + * only useful for debugging. + */ + virtual std::string ToExpression() const noexcept = 0; + + gcc_pure + virtual bool Match(const LightSong &song) const noexcept = 0; +}; + +class StringFilter { + std::string value; + + /** + * This value is only set if case folding is enabled. + */ + IcuCompare fold_case; + +public: + template<typename V> + StringFilter(V &&_value, bool _fold_case) + :value(std::forward<V>(_value)), + fold_case(_fold_case + ? IcuCompare(value.c_str()) + : IcuCompare()) {} + + bool empty() const noexcept { + return value.empty(); + } + + const auto &GetValue() const noexcept { + return value; + } + + bool GetFoldCase() const noexcept { + return fold_case; + } + + gcc_pure + bool Match(const char *s) const noexcept; +}; + +class UriSongFilter final : public ISongFilter { + StringFilter filter; + + bool negated; + +public: + template<typename V> + UriSongFilter(V &&_value, bool fold_case, bool _negated) + :filter(std::forward<V>(_value), fold_case), + negated(_negated) {} + + const auto &GetValue() const noexcept { + return filter.GetValue(); + } + + bool GetFoldCase() const { + return filter.GetFoldCase(); + } + + bool IsNegated() const noexcept { + return negated; + } + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique<UriSongFilter>(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; + +class BaseSongFilter final : public ISongFilter { + std::string value; + +public: + BaseSongFilter(const BaseSongFilter &) = default; + + template<typename V> + explicit BaseSongFilter(V &&_value) + :value(std::forward<V>(_value)) {} + + const char *GetValue() const noexcept { + return value.c_str(); + } + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique<BaseSongFilter>(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; + +class TagSongFilter final : public ISongFilter { + TagType type; + + bool negated; + + StringFilter filter; + +public: + template<typename V> + TagSongFilter(TagType _type, V &&_value, bool fold_case, bool _negated) + :type(_type), negated(_negated), + filter(std::forward<V>(_value), fold_case) {} + + TagType GetTagType() const { + return type; + } + + const auto &GetValue() const noexcept { + return filter.GetValue(); + } + + bool GetFoldCase() const { + return filter.GetFoldCase(); + } + + bool IsNegated() const noexcept { + return negated; + } + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique<TagSongFilter>(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; + +private: + bool MatchNN(const Tag &tag) const noexcept; + bool MatchNN(const TagItem &tag) const noexcept; +}; + +class ModifiedSinceSongFilter final : public ISongFilter { + std::chrono::system_clock::time_point value; + +public: + explicit ModifiedSinceSongFilter(std::chrono::system_clock::time_point _value) noexcept + :value(_value) {} + + ISongFilterPtr Clone() const noexcept override { + return std::make_unique<ModifiedSinceSongFilter>(*this); + } + + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; + +/** + * Combine multiple #ISongFilter instances with logical "and". + */ +class AndSongFilter final : public ISongFilter { + std::list<ISongFilterPtr> items; + +public: + const auto &GetItems() const noexcept { + return items; + } + + template<typename I> + void AddItem(I &&_item) { + items.emplace_back(std::forward<I>(_item)); + } + + gcc_pure + bool IsEmpty() const noexcept { + return items.empty(); + } + + /* virtual methods from ISongFilter */ + ISongFilterPtr Clone() const noexcept override; + std::string ToExpression() const noexcept override; + bool Match(const LightSong &song) const noexcept override; +}; + +class SongFilter { + AndSongFilter and_filter; + +public: + SongFilter() = default; + + gcc_nonnull(3) + SongFilter(TagType tag, const char *value, bool fold_case=false); + + ~SongFilter(); + + SongFilter(SongFilter &&) = default; + SongFilter &operator=(SongFilter &&) = default; + + /** + * Convert this object into an "expression". This is + * only useful for debugging. + */ + std::string ToExpression() const noexcept; + +private: + static ISongFilterPtr ParseExpression(const char *&s, bool fold_case=false); + + gcc_nonnull(2,3) + void Parse(const char *tag, const char *value, bool fold_case=false); + +public: + /** + * Throws on error. + */ + void Parse(ConstBuffer<const char *> args, bool fold_case=false); + + gcc_pure + bool Match(const LightSong &song) const noexcept; + + const auto &GetItems() const noexcept { + return and_filter.GetItems(); + } + + gcc_pure + bool IsEmpty() const noexcept { + return and_filter.IsEmpty(); + } + + /** + * Is there at least one item with "fold case" enabled? + */ + gcc_pure + bool HasFoldCase() const noexcept; + + /** + * Does this filter contain constraints other than "base"? + */ + gcc_pure + bool HasOtherThanBase() const noexcept; + + /** + * Returns the "base" specification (if there is one) or + * nullptr. + */ + gcc_pure + const char *GetBase() const noexcept; + + /** + * Create a copy of the filter with the given prefix stripped + * from all #LOCATE_TAG_BASE_TYPE items. This is used to + * filter songs in mounted databases. + */ + SongFilter WithoutBasePrefix(const char *prefix) const noexcept; +}; + +#endif diff --git a/src/song/LightSong.cxx b/src/song/LightSong.cxx new file mode 100644 index 000000000..b1a219e0b --- /dev/null +++ b/src/song/LightSong.cxx @@ -0,0 +1,35 @@ +/* + * Copyright 2003-2017 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "LightSong.hxx" +#include "tag/Tag.hxx" + +SignedSongTime +LightSong::GetDuration() const noexcept +{ + SongTime a = start_time, b = end_time; + if (!b.IsPositive()) { + if (tag.duration.IsNegative()) + return tag.duration; + + b = SongTime(tag.duration); + } + + return SignedSongTime(b - a); +} diff --git a/src/song/LightSong.hxx b/src/song/LightSong.hxx new file mode 100644 index 000000000..7bc21e030 --- /dev/null +++ b/src/song/LightSong.hxx @@ -0,0 +1,106 @@ +/* + * Copyright 2003-2017 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_LIGHT_SONG_HXX +#define MPD_LIGHT_SONG_HXX + +#include "Chrono.hxx" +#include "AudioFormat.hxx" +#include "Compiler.h" + +#include <string> +#include <chrono> + +struct Tag; + +/** + * A reference to a song file. Unlike the other "Song" classes in the + * MPD code base, this one consists only of pointers. It is supposed + * to be as light as possible while still providing all the + * information MPD has about a song file. This class does not manage + * any memory, and the pointers become invalid quickly. Only to be + * used to pass around during well-defined situations. + */ +struct LightSong { + /** + * If this is not nullptr, then it denotes a prefix for the + * #uri. To build the full URI, join directory and uri with a + * slash. + */ + const char *directory = nullptr; + + const char *uri; + + /** + * The "real" URI, the one to be used for opening the + * resource. If this attribute is nullptr, then #uri (and + * #directory) shall be used. + * + * This attribute is used for songs from the database which + * have a relative URI. + */ + const char *real_uri = nullptr; + + /** + * Metadata. + */ + const Tag &tag; + + /** + * The time stamp of the last file modification. A negative + * value means that this is unknown/unavailable. + */ + std::chrono::system_clock::time_point mtime = std::chrono::system_clock::time_point::min(); + + /** + * Start of this sub-song within the file. + */ + SongTime start_time = SongTime::zero(); + + /** + * End of this sub-song within the file. + * Unused if zero. + */ + SongTime end_time = SongTime::zero(); + + /** + * The audio format of the song, if given by the decoder + * plugin. May be undefined if unknown. + */ + AudioFormat audio_format = AudioFormat::Undefined(); + + LightSong(const char *_uri, const Tag &_tag) noexcept + :uri(_uri), tag(_tag) {} + + gcc_pure + std::string GetURI() const noexcept { + if (directory == nullptr) + return std::string(uri); + + std::string result(directory); + result.push_back('/'); + result.append(uri); + return result; + } + + gcc_pure + SignedSongTime GetDuration() const noexcept; +}; + +#endif |