diff options
Diffstat (limited to 'xbmc/playlists/SmartPlayList.cpp')
-rw-r--r-- | xbmc/playlists/SmartPlayList.cpp | 1610 |
1 files changed, 1610 insertions, 0 deletions
diff --git a/xbmc/playlists/SmartPlayList.cpp b/xbmc/playlists/SmartPlayList.cpp new file mode 100644 index 0000000..dc83a75 --- /dev/null +++ b/xbmc/playlists/SmartPlayList.cpp @@ -0,0 +1,1610 @@ +/* + * Copyright (C) 2005-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 "SmartPlayList.h" + +#include "ServiceBroker.h" +#include "Util.h" +#include "dbwrappers/Database.h" +#include "filesystem/File.h" +#include "filesystem/SmartPlaylistDirectory.h" +#include "guilib/LocalizeStrings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/DatabaseUtils.h" +#include "utils/JSONVariantParser.h" +#include "utils/JSONVariantWriter.h" +#include "utils/StreamDetails.h" +#include "utils/StringUtils.h" +#include "utils/StringValidation.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XMLUtils.h" +#include "utils/log.h" + +#include <cstdlib> +#include <memory> +#include <set> +#include <string> +#include <vector> + +using namespace XFILE; + +typedef struct +{ + char string[17]; + Field field; + CDatabaseQueryRule::FIELD_TYPE type; + StringValidation::Validator validator; + bool browseable; + int localizedString; +} translateField; + +// clang-format off +static const translateField fields[] = { + { "none", FieldNone, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 231 }, + { "filename", FieldFilename, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 561 }, + { "path", FieldPath, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 573 }, + { "album", FieldAlbum, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 558 }, + { "albumartist", FieldAlbumArtist, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 566 }, + { "artist", FieldArtist, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 557 }, + { "tracknumber", FieldTrackNumber, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 554 }, + { "role", FieldRole, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 38033 }, + { "comment", FieldComment, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 569 }, + { "review", FieldReview, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 183 }, + { "themes", FieldThemes, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21895 }, + { "moods", FieldMoods, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 175 }, + { "styles", FieldStyles, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 176 }, + { "type", FieldAlbumType, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 564 }, + { "compilation", FieldCompilation, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 204 }, + { "label", FieldMusicLabel, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21899 }, + { "title", FieldTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 556 }, + { "sorttitle", FieldSortTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 171 }, + { "originaltitle", FieldOriginalTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 20376 }, + { "year", FieldYear, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, true, 562 }, + { "time", FieldTime, CDatabaseQueryRule::SECONDS_FIELD, StringValidation::IsTime, false, 180 }, + { "playcount", FieldPlaycount, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 567 }, + { "lastplayed", FieldLastPlayed, CDatabaseQueryRule::DATE_FIELD, NULL, false, 568 }, + { "inprogress", FieldInProgress, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 575 }, + { "rating", FieldRating, CDatabaseQueryRule::REAL_FIELD, CSmartPlaylistRule::ValidateRating, false, 563 }, + { "userrating", FieldUserRating, CDatabaseQueryRule::REAL_FIELD, CSmartPlaylistRule::ValidateMyRating, false, 38018 }, + { "votes", FieldVotes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 205 }, + { "top250", FieldTop250, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 13409 }, + { "mpaarating", FieldMPAA, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 20074 }, + { "dateadded", FieldDateAdded, CDatabaseQueryRule::DATE_FIELD, NULL, false, 570 }, + { "datemodified", FieldDateModified, CDatabaseQueryRule::DATE_FIELD, NULL, false, 39119 }, + { "datenew", FieldDateNew, CDatabaseQueryRule::DATE_FIELD, NULL, false, 21877 }, + { "genre", FieldGenre, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 515 }, + { "plot", FieldPlot, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 207 }, + { "plotoutline", FieldPlotOutline, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 203 }, + { "tagline", FieldTagline, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 202 }, + { "set", FieldSet, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20457 }, + { "director", FieldDirector, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20339 }, + { "actor", FieldActor, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20337 }, + { "writers", FieldWriter, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20417 }, + { "airdate", FieldAirDate, CDatabaseQueryRule::DATE_FIELD, NULL, false, 20416 }, + { "hastrailer", FieldTrailer, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 20423 }, + { "studio", FieldStudio, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 572 }, + { "country", FieldCountry, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 574 }, + { "tvshow", FieldTvShowTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20364 }, + { "status", FieldTvShowStatus, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 126 }, + { "season", FieldSeason, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 20373 }, + { "episode", FieldEpisodeNumber, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 20359 }, + { "numepisodes", FieldNumberOfEpisodes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 20360 }, + { "numwatched", FieldNumberOfWatchedEpisodes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21457 }, + { "videoresolution", FieldVideoResolution, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21443 }, + { "videocodec", FieldVideoCodec, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21445 }, + { "videoaspect", FieldVideoAspectRatio, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21374 }, + { "audiochannels", FieldAudioChannels, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21444 }, + { "audiocodec", FieldAudioCodec, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21446 }, + { "audiolanguage", FieldAudioLanguage, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21447 }, + { "audiocount", FieldAudioCount, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21481 }, + { "subtitlecount", FieldSubtitleCount, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21482 }, + { "subtitlelanguage", FieldSubtitleLanguage, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21448 }, + { "random", FieldRandom, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 590 }, + { "playlist", FieldPlaylist, CDatabaseQueryRule::PLAYLIST_FIELD, NULL, true, 559 }, + { "virtualfolder", FieldVirtualFolder, CDatabaseQueryRule::PLAYLIST_FIELD, NULL, true, 614 }, + { "tag", FieldTag, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20459 }, + { "instruments", FieldInstruments, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21892 }, + { "biography", FieldBiography, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21887 }, + { "born", FieldBorn, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21893 }, + { "bandformed", FieldBandFormed, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21894 }, + { "disbanded", FieldDisbanded, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21896 }, + { "died", FieldDied, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21897 }, + { "artisttype", FieldArtistType, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 564 }, + { "gender", FieldGender, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 39025 }, + { "disambiguation", FieldDisambiguation, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 39026 }, + { "source", FieldSource, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 39030 }, + { "disctitle", FieldDiscTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 38076 }, + { "isboxset", FieldIsBoxset, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 38074 }, + { "totaldiscs", FieldTotalDiscs, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 38077 }, + { "originalyear", FieldOrigYear, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, true, 38078 }, + { "bpm", FieldBPM, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 38080 }, + { "samplerate", FieldSampleRate, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 613 }, + { "bitrate", FieldMusicBitRate, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 623 }, + { "channels", FieldNoOfChannels, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 253 }, + { "albumstatus", FieldAlbumStatus, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 38081 }, + { "albumduration", FieldAlbumDuration, CDatabaseQueryRule::SECONDS_FIELD, StringValidation::IsTime, false, 180 }, + { "hdrtype", FieldHdrType, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 20474 }, +}; +// clang-format on + +typedef struct +{ + std::string name; + Field field; + bool canMix; + int localizedString; +} group; + +// clang-format off +static const group groups[] = { { "", FieldUnknown, false, 571 }, + { "none", FieldNone, false, 231 }, + { "sets", FieldSet, true, 20434 }, + { "genres", FieldGenre, false, 135 }, + { "years", FieldYear, false, 652 }, + { "actors", FieldActor, false, 344 }, + { "directors", FieldDirector, false, 20348 }, + { "writers", FieldWriter, false, 20418 }, + { "studios", FieldStudio, false, 20388 }, + { "countries", FieldCountry, false, 20451 }, + { "artists", FieldArtist, false, 133 }, + { "albums", FieldAlbum, false, 132 }, + { "tags", FieldTag, false, 20459 }, + { "originalyears", FieldOrigYear, false, 38078 }, + }; +// clang-format on + +#define RULE_VALUE_SEPARATOR " / " + +CSmartPlaylistRule::CSmartPlaylistRule() = default; + +int CSmartPlaylistRule::TranslateField(const char *field) const +{ + for (const translateField& f : fields) + if (StringUtils::EqualsNoCase(field, f.string)) return f.field; + return FieldNone; +} + +std::string CSmartPlaylistRule::TranslateField(int field) const +{ + for (const translateField& f : fields) + if (field == f.field) return f.string; + return "none"; +} + +SortBy CSmartPlaylistRule::TranslateOrder(const char *order) +{ + return SortUtils::SortMethodFromString(order); +} + +std::string CSmartPlaylistRule::TranslateOrder(SortBy order) +{ + std::string sortOrder = SortUtils::SortMethodToString(order); + if (sortOrder.empty()) + return "none"; + + return sortOrder; +} + +Field CSmartPlaylistRule::TranslateGroup(const char *group) +{ + for (const auto & i : groups) + { + if (StringUtils::EqualsNoCase(group, i.name)) + return i.field; + } + + return FieldUnknown; +} + +std::string CSmartPlaylistRule::TranslateGroup(Field group) +{ + for (const auto & i : groups) + { + if (group == i.field) + return i.name; + } + + return ""; +} + +std::string CSmartPlaylistRule::GetLocalizedField(int field) +{ + for (const translateField& f : fields) + if (field == f.field) return g_localizeStrings.Get(f.localizedString); + return g_localizeStrings.Get(16018); +} + +CDatabaseQueryRule::FIELD_TYPE CSmartPlaylistRule::GetFieldType(int field) const +{ + for (const translateField& f : fields) + if (field == f.field) return f.type; + return TEXT_FIELD; +} + +bool CSmartPlaylistRule::IsFieldBrowseable(int field) +{ + for (const translateField& f : fields) + if (field == f.field) return f.browseable; + + return false; +} + +bool CSmartPlaylistRule::Validate(const std::string &input, void *data) +{ + if (data == NULL) + return true; + + CSmartPlaylistRule *rule = static_cast<CSmartPlaylistRule*>(data); + + // check if there's a validator for this rule + StringValidation::Validator validator = NULL; + for (const translateField& field : fields) + { + if (rule->m_field == field.field) + { + validator = field.validator; + break; + } + } + if (validator == NULL) + return true; + + // split the input into multiple values and validate every value separately + std::vector<std::string> values = StringUtils::Split(input, RULE_VALUE_SEPARATOR); + for (std::vector<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) + { + if (!validator(*it, data)) + return false; + } + + return true; +} + +bool CSmartPlaylistRule::ValidateRating(const std::string &input, void *data) +{ + char *end = NULL; + std::string strRating = input; + StringUtils::Trim(strRating); + + double rating = std::strtod(strRating.c_str(), &end); + return (end == NULL || *end == '\0') && + rating >= 0.0 && rating <= 10.0; +} + +bool CSmartPlaylistRule::ValidateMyRating(const std::string &input, void *data) +{ + std::string strRating = input; + StringUtils::Trim(strRating); + + int rating = atoi(strRating.c_str()); + return StringValidation::IsPositiveInteger(input, data) && rating <= 10; +} + +std::vector<Field> CSmartPlaylistRule::GetFields(const std::string &type) +{ + std::vector<Field> fields; + bool isVideo = false; + if (type == "mixed") + { + fields.push_back(FieldGenre); + fields.push_back(FieldAlbum); + fields.push_back(FieldArtist); + fields.push_back(FieldAlbumArtist); + fields.push_back(FieldTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldYear); + fields.push_back(FieldTime); + fields.push_back(FieldTrackNumber); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + } + else if (type == "songs") + { + fields.push_back(FieldGenre); + fields.push_back(FieldSource); + fields.push_back(FieldAlbum); + fields.push_back(FieldDiscTitle); + fields.push_back(FieldArtist); + fields.push_back(FieldAlbumArtist); + fields.push_back(FieldTitle); + fields.push_back(FieldYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + fields.push_back(FieldOrigYear); + fields.push_back(FieldTime); + fields.push_back(FieldTrackNumber); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldComment); + fields.push_back(FieldMoods); + fields.push_back(FieldBPM); + fields.push_back(FieldSampleRate); + fields.push_back(FieldMusicBitRate); + fields.push_back(FieldNoOfChannels); + fields.push_back(FieldDateAdded); + fields.push_back(FieldDateModified); + fields.push_back(FieldDateNew); + } + else if (type == "albums") + { + fields.push_back(FieldGenre); + fields.push_back(FieldSource); + fields.push_back(FieldAlbum); + fields.push_back(FieldDiscTitle); + fields.push_back(FieldTotalDiscs); + fields.push_back(FieldIsBoxset); + fields.push_back(FieldArtist); // any artist + fields.push_back(FieldAlbumArtist); // album artist + fields.push_back(FieldYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + fields.push_back(FieldOrigYear); + fields.push_back(FieldAlbumDuration); + fields.push_back(FieldReview); + fields.push_back(FieldThemes); + fields.push_back(FieldMoods); + fields.push_back(FieldStyles); + fields.push_back(FieldCompilation); + fields.push_back(FieldAlbumType); + fields.push_back(FieldMusicLabel); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldPath); + fields.push_back(FieldAlbumStatus); + fields.push_back(FieldDateAdded); + fields.push_back(FieldDateModified); + fields.push_back(FieldDateNew); + } + else if (type == "artists") + { + fields.push_back(FieldArtist); + fields.push_back(FieldSource); + fields.push_back(FieldGenre); + fields.push_back(FieldMoods); + fields.push_back(FieldStyles); + fields.push_back(FieldInstruments); + fields.push_back(FieldBiography); + fields.push_back(FieldArtistType); + fields.push_back(FieldGender); + fields.push_back(FieldDisambiguation); + fields.push_back(FieldBorn); + fields.push_back(FieldBandFormed); + fields.push_back(FieldDisbanded); + fields.push_back(FieldDied); + fields.push_back(FieldRole); + fields.push_back(FieldPath); + fields.push_back(FieldDateAdded); + fields.push_back(FieldDateModified); + fields.push_back(FieldDateNew); + } + else if (type == "tvshows") + { + fields.push_back(FieldTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldPlot); + fields.push_back(FieldTvShowStatus); + fields.push_back(FieldVotes); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldYear); + fields.push_back(FieldGenre); + fields.push_back(FieldDirector); + fields.push_back(FieldActor); + fields.push_back(FieldNumberOfEpisodes); + fields.push_back(FieldNumberOfWatchedEpisodes); + fields.push_back(FieldPlaycount); + fields.push_back(FieldPath); + fields.push_back(FieldStudio); + fields.push_back(FieldMPAA); + fields.push_back(FieldDateAdded); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldInProgress); + fields.push_back(FieldTag); + } + else if (type == "episodes") + { + fields.push_back(FieldTitle); + fields.push_back(FieldTvShowTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldPlot); + fields.push_back(FieldVotes); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldTime); + fields.push_back(FieldWriter); + fields.push_back(FieldAirDate); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldInProgress); + fields.push_back(FieldGenre); + fields.push_back(FieldYear); // premiered + fields.push_back(FieldDirector); + fields.push_back(FieldActor); + fields.push_back(FieldEpisodeNumber); + fields.push_back(FieldSeason); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldStudio); + fields.push_back(FieldMPAA); + fields.push_back(FieldDateAdded); + fields.push_back(FieldTag); + isVideo = true; + } + else if (type == "movies") + { + fields.push_back(FieldTitle); + fields.push_back(FieldOriginalTitle); + fields.push_back(FieldPlot); + fields.push_back(FieldPlotOutline); + fields.push_back(FieldTagline); + fields.push_back(FieldVotes); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldTime); + fields.push_back(FieldWriter); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldInProgress); + fields.push_back(FieldGenre); + fields.push_back(FieldCountry); + fields.push_back(FieldYear); // premiered + fields.push_back(FieldDirector); + fields.push_back(FieldActor); + fields.push_back(FieldMPAA); + fields.push_back(FieldTop250); + fields.push_back(FieldStudio); + fields.push_back(FieldTrailer); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldSet); + fields.push_back(FieldTag); + fields.push_back(FieldDateAdded); + isVideo = true; + } + else if (type == "musicvideos") + { + fields.push_back(FieldTitle); + fields.push_back(FieldGenre); + fields.push_back(FieldAlbum); + fields.push_back(FieldYear); + fields.push_back(FieldArtist); + fields.push_back(FieldFilename); + fields.push_back(FieldPath); + fields.push_back(FieldPlaycount); + fields.push_back(FieldLastPlayed); + fields.push_back(FieldRating); + fields.push_back(FieldUserRating); + fields.push_back(FieldTime); + fields.push_back(FieldDirector); + fields.push_back(FieldStudio); + fields.push_back(FieldPlot); + fields.push_back(FieldTag); + fields.push_back(FieldDateAdded); + isVideo = true; + } + if (isVideo) + { + fields.push_back(FieldVideoResolution); + fields.push_back(FieldAudioChannels); + fields.push_back(FieldAudioCount); + fields.push_back(FieldSubtitleCount); + fields.push_back(FieldVideoCodec); + fields.push_back(FieldAudioCodec); + fields.push_back(FieldAudioLanguage); + fields.push_back(FieldSubtitleLanguage); + fields.push_back(FieldVideoAspectRatio); + fields.push_back(FieldHdrType); + } + fields.push_back(FieldPlaylist); + fields.push_back(FieldVirtualFolder); + + return fields; +} + +std::vector<SortBy> CSmartPlaylistRule::GetOrders(const std::string &type) +{ + std::vector<SortBy> orders; + orders.push_back(SortByNone); + if (type == "mixed") + { + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByArtist); + orders.push_back(SortByTitle); + orders.push_back(SortByYear); + orders.push_back(SortByTime); + orders.push_back(SortByTrackNumber); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + } + else if (type == "songs") + { + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByArtist); + orders.push_back(SortByTitle); + orders.push_back(SortByYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + orders.push_back(SortByOrigDate); + orders.push_back(SortByTime); + orders.push_back(SortByTrackNumber); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByDateAdded); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByBPM); + } + else if (type == "albums") + { + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByTotalDiscs); + orders.push_back(SortByArtist); // any artist + orders.push_back(SortByYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + orders.push_back(SortByOrigDate); + //orders.push_back(SortByThemes); + //orders.push_back(SortByMoods); + //orders.push_back(SortByStyles); + orders.push_back(SortByAlbumType); + //orders.push_back(SortByMusicLabel); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByDateAdded); + } + else if (type == "artists") + { + orders.push_back(SortByArtist); + } + else if (type == "tvshows") + { + orders.push_back(SortBySortTitle); + orders.push_back(SortByOriginalTitle); + orders.push_back(SortByTvShowStatus); + orders.push_back(SortByVotes); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByYear); + orders.push_back(SortByGenre); + orders.push_back(SortByNumberOfEpisodes); + orders.push_back(SortByNumberOfWatchedEpisodes); + //orders.push_back(SortByPlaycount); + orders.push_back(SortByPath); + orders.push_back(SortByStudio); + orders.push_back(SortByMPAA); + orders.push_back(SortByDateAdded); + orders.push_back(SortByLastPlayed); + } + else if (type == "episodes") + { + orders.push_back(SortByTitle); + orders.push_back(SortByOriginalTitle); + orders.push_back(SortByTvShowTitle); + orders.push_back(SortByVotes); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByTime); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByYear); // premiered/dateaired + orders.push_back(SortByEpisodeNumber); + orders.push_back(SortBySeason); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByStudio); + orders.push_back(SortByMPAA); + orders.push_back(SortByDateAdded); + } + else if (type == "movies") + { + orders.push_back(SortBySortTitle); + orders.push_back(SortByOriginalTitle); + orders.push_back(SortByVotes); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByTime); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByGenre); + orders.push_back(SortByCountry); + orders.push_back(SortByYear); // premiered + orders.push_back(SortByMPAA); + orders.push_back(SortByTop250); + orders.push_back(SortByStudio); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByDateAdded); + } + else if (type == "musicvideos") + { + orders.push_back(SortByTitle); + orders.push_back(SortByGenre); + orders.push_back(SortByAlbum); + orders.push_back(SortByYear); + orders.push_back(SortByArtist); + orders.push_back(SortByFile); + orders.push_back(SortByPath); + orders.push_back(SortByPlaycount); + orders.push_back(SortByLastPlayed); + orders.push_back(SortByTime); + orders.push_back(SortByRating); + orders.push_back(SortByUserRating); + orders.push_back(SortByStudio); + orders.push_back(SortByDateAdded); + } + orders.push_back(SortByRandom); + + return orders; +} + +std::vector<Field> CSmartPlaylistRule::GetGroups(const std::string &type) +{ + std::vector<Field> groups; + groups.push_back(FieldUnknown); + + if (type == "artists") + groups.push_back(FieldGenre); + else if (type == "albums") + { + groups.push_back(FieldYear); + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + groups.push_back(FieldOrigYear); + } + if (type == "movies") + { + groups.push_back(FieldNone); + groups.push_back(FieldSet); + groups.push_back(FieldGenre); + groups.push_back(FieldYear); + groups.push_back(FieldActor); + groups.push_back(FieldDirector); + groups.push_back(FieldWriter); + groups.push_back(FieldStudio); + groups.push_back(FieldCountry); + groups.push_back(FieldTag); + } + else if (type == "tvshows") + { + groups.push_back(FieldGenre); + groups.push_back(FieldYear); + groups.push_back(FieldActor); + groups.push_back(FieldDirector); + groups.push_back(FieldStudio); + groups.push_back(FieldTag); + } + else if (type == "musicvideos") + { + groups.push_back(FieldArtist); + groups.push_back(FieldAlbum); + groups.push_back(FieldGenre); + groups.push_back(FieldYear); + groups.push_back(FieldDirector); + groups.push_back(FieldStudio); + groups.push_back(FieldTag); + } + + return groups; +} + +std::string CSmartPlaylistRule::GetLocalizedGroup(Field group) +{ + for (const auto & i : groups) + { + if (group == i.field) + return g_localizeStrings.Get(i.localizedString); + } + + return g_localizeStrings.Get(groups[0].localizedString); +} + +bool CSmartPlaylistRule::CanGroupMix(Field group) +{ + for (const auto & i : groups) + { + if (group == i.field) + return i.canMix; + } + + return false; +} + +std::string CSmartPlaylistRule::GetLocalizedRule() const +{ + return StringUtils::Format("{} {} {}", GetLocalizedField(m_field), + GetLocalizedOperator(m_operator), GetParameter()); +} + +std::string CSmartPlaylistRule::GetVideoResolutionQuery(const std::string ¶meter) const +{ + std::string retVal(" IN (SELECT DISTINCT idFile FROM streamdetails WHERE iVideoWidth "); + int iRes = (int)std::strtol(parameter.c_str(), NULL, 10); + + int min, max; + if (iRes >= 2160) + { + min = 1921; + max = INT_MAX; + } + else if (iRes >= 1080) { min = 1281; max = 1920; } + else if (iRes >= 720) { min = 961; max = 1280; } + else if (iRes >= 540) { min = 721; max = 960; } + else { min = 0; max = 720; } + + switch (m_operator) + { + case OPERATOR_EQUALS: + retVal += StringUtils::Format(">= {} AND iVideoWidth <= {}", min, max); + break; + case OPERATOR_DOES_NOT_EQUAL: + retVal += StringUtils::Format("< {} OR iVideoWidth > {}", min, max); + break; + case OPERATOR_LESS_THAN: + retVal += StringUtils::Format("< {}", min); + break; + case OPERATOR_GREATER_THAN: + retVal += StringUtils::Format("> {}", max); + break; + default: + break; + } + + retVal += ")"; + return retVal; +} + +std::string CSmartPlaylistRule::GetBooleanQuery(const std::string &negate, const std::string &strType) const +{ + if (strType == "movies") + { + if (m_field == FieldInProgress) + return "movie_view.idFile " + negate + " IN (SELECT DISTINCT idFile FROM bookmark WHERE type = 1)"; + else if (m_field == FieldTrailer) + return negate + GetField(m_field, strType) + "!= ''"; + } + else if (strType == "episodes") + { + if (m_field == FieldInProgress) + return "episode_view.idFile " + negate + " IN (SELECT DISTINCT idFile FROM bookmark WHERE type = 1)"; + } + else if (strType == "tvshows") + { + if (m_field == FieldInProgress) + return negate + " (" + "(tvshow_view.watchedcount > 0 AND tvshow_view.watchedcount < tvshow_view.totalCount) OR " + "(tvshow_view.watchedcount = 0 AND EXISTS " + "(SELECT 1 FROM episode_view WHERE episode_view.idShow = " + GetField(FieldId, strType) + " AND episode_view.resumeTimeInSeconds > 0)" + ")" + ")"; + } + if (strType == "albums") + { + if (m_field == FieldCompilation) + return negate + GetField(m_field, strType); + if (m_field == FieldIsBoxset) + return negate + "albumview.bBoxedSet = 1"; + } + return ""; +} + +CDatabaseQueryRule::SEARCH_OPERATOR CSmartPlaylistRule::GetOperator(const std::string &strType) const +{ + SEARCH_OPERATOR op = CDatabaseQueryRule::GetOperator(strType); + if ((strType == "tvshows" || strType == "episodes") && m_field == FieldYear) + { // special case for premiered which is a date rather than a year + //! @todo SMARTPLAYLISTS do we really need this, or should we just make this field the premiered date and request a date? + if (op == OPERATOR_EQUALS) + op = OPERATOR_CONTAINS; + else if (op == OPERATOR_DOES_NOT_EQUAL) + op = OPERATOR_DOES_NOT_CONTAIN; + } + return op; +} + +std::string CSmartPlaylistRule::FormatParameter(const std::string &operatorString, const std::string ¶m, const CDatabase &db, const std::string &strType) const +{ + // special-casing + if (m_field == FieldTime || m_field == FieldAlbumDuration) + { // translate time to seconds + std::string seconds = std::to_string(StringUtils::TimeStringToSeconds(param)); + return db.PrepareSQL(operatorString, seconds.c_str()); + } + return CDatabaseQueryRule::FormatParameter(operatorString, param, db, strType); +} + +std::string CSmartPlaylistRule::FormatLinkQuery(const char *field, const char *table, const MediaType& mediaType, const std::string& mediaField, const std::string& parameter) +{ + // NOTE: no need for a PrepareSQL here, as the parameter has already been formatted + return StringUtils::Format( + " EXISTS (SELECT 1 FROM {}_link" + " JOIN {} ON {}.{}_id={}_link.{}_id" + " WHERE {}_link.media_id={} AND {}.name {} AND {}_link.media_type = '{}')", + field, table, table, table, field, table, field, mediaField, table, parameter, field, + mediaType); +} + +std::string CSmartPlaylistRule::FormatYearQuery(const std::string& field, + const std::string& param, + const std::string& parameter) const +{ + std::string query; + if (m_operator == OPERATOR_EQUALS && param == "0") + query = "(TRIM(" + field + ") = '' OR " + field + " IS NULL)"; + else if (m_operator == OPERATOR_DOES_NOT_EQUAL && param == "0") + query = "(TRIM(" + field + ") <> '' AND " + field + " IS NOT NULL)"; + else + { // Get year from ISO8601 date string, cast as INTEGER + query = "CAST(" + field + " as INTEGER)" + parameter; + if (m_operator == OPERATOR_LESS_THAN) + query = "(TRIM(" + field + ") = '' OR " + field + " IS NULL OR " + query + ")"; + } + return query; +} + +std::string CSmartPlaylistRule::FormatWhereClause(const std::string &negate, const std::string &oper, const std::string ¶m, + const CDatabase &db, const std::string &strType) const +{ + std::string parameter = FormatParameter(oper, param, db, strType); + + std::string query; + std::string table; + if (strType == "songs") + { + table = "songview"; + + if (m_field == FieldGenre) + query = negate + " EXISTS (SELECT 1 FROM song_genre, genre WHERE song_genre.idSong = " + GetField(FieldId, strType) + " AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")"; + else if (m_field == FieldArtist) + query = negate + " EXISTS (SELECT 1 FROM song_artist, artist WHERE song_artist.idSong = " + GetField(FieldId, strType) + " AND song_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldAlbumArtist) + query = negate + " EXISTS (SELECT 1 FROM album_artist, artist WHERE album_artist.idAlbum = " + table + ".idAlbum AND album_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldLastPlayed && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " is NULL or " + GetField(m_field, strType) + parameter; + else if (m_field == FieldSource) + query = negate + " EXISTS (SELECT 1 FROM album_source, source WHERE album_source.idAlbum = " + table + ".idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + ")"; + else if (m_field == FieldYear || m_field == FieldOrigYear) + { + std::string field; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + field = GetField(FieldOrigYear, strType); + else + field = GetField(m_field, strType); + query = FormatYearQuery(field, param, parameter); + } + } + else if (strType == "albums") + { + table = "albumview"; + + if (m_field == FieldGenre) + query = negate + " EXISTS (SELECT 1 FROM song, song_genre, genre WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND song.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")"; + else if (m_field == FieldArtist) + query = negate + " EXISTS (SELECT 1 FROM song, song_artist, artist WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND song.idSong = song_artist.idSong AND song_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldAlbumArtist) + query = negate + " EXISTS (SELECT 1 FROM album_artist, artist WHERE album_artist.idAlbum = " + GetField(FieldId, strType) + " AND album_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")"; + else if (m_field == FieldPath) + query = negate + " EXISTS (SELECT 1 FROM song JOIN path on song.idpath = path.idpath WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND path.strPath" + parameter + ")"; + else if (m_field == FieldLastPlayed && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " is NULL or " + GetField(m_field, strType) + parameter; + else if (m_field == FieldSource) + query = negate + " EXISTS (SELECT 1 FROM album_source, source WHERE album_source.idAlbum = " + GetField(FieldId, strType) + " AND album_source.idSource = source.idSource AND source.strName" + parameter + ")"; + else if (m_field == FieldDiscTitle) + query = negate + + " EXISTS (SELECT 1 FROM song WHERE song.idAlbum = " + GetField(FieldId, strType) + + " AND song.strDiscSubtitle" + parameter + ")"; + else if (m_field == FieldYear || m_field == FieldOrigYear) + { + std::string field; + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE)) + field = GetField(FieldOrigYear, strType); + else + field = GetField(m_field, strType); + query = FormatYearQuery(field, param, parameter); + } + } + else if (strType == "artists") + { + table = "artistview"; + + if (m_field == FieldGenre) + { + query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist, song_genre, genre WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song_artist.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")"; + query += " OR "; + query += "EXISTS (SELECT DISTINCT album_artist.idArtist FROM album_artist, song, song_genre, genre WHERE album_artist.idArtist = " + GetField(FieldId, strType) + " AND song.idAlbum = album_artist.idAlbum AND song.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + "))"; + } + else if (m_field == FieldRole) + { + query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist, role WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song_artist.idRole = role.idRole AND role.strRole" + parameter + "))"; + } + else if (m_field == FieldPath) + { + query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong JOIN path ON song.idpath = path.idpath "; + query += "WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND path.strPath" + parameter + "))"; + } + else if (m_field == FieldSource) + { + query = negate + " (EXISTS(SELECT 1 FROM song_artist, song, album_source, source WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song.idSong = song_artist.idSong AND song_artist.idRole = 1 AND album_source.idAlbum = song.idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + ")"; + query += " OR "; + query += " EXISTS (SELECT 1 FROM album_artist, album_source, source WHERE album_artist.idArtist = " + GetField(FieldId, strType) + " AND album_source.idAlbum = album_artist.idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + "))"; + } + } + else if (strType == "movies") + { + table = "movie_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldActor) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldWriter) + query = negate + FormatLinkQuery("writer", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if (m_field == FieldCountry) + query = negate + FormatLinkQuery("country", "country", MediaTypeMovie, GetField(FieldId, strType), parameter); + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeMovie, GetField(FieldId, strType), parameter); + } + else if (strType == "musicvideos") + { + table = "musicvideo_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if (m_field == FieldArtist || m_field == FieldAlbumArtist) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeMusicVideo, GetField(FieldId, strType), parameter); + } + else if (strType == "tvshows") + { + table = "tvshow_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldActor) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeTvShow, GetField(FieldId, strType), parameter); + else if (m_field == FieldMPAA) + query = negate + " (" + GetField(m_field, strType) + parameter + ")"; + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldPlaycount) + query = "CASE WHEN COALESCE(" + GetField(FieldNumberOfEpisodes, strType) + " - " + GetField(FieldNumberOfWatchedEpisodes, strType) + ", 0) > 0 THEN 0 ELSE 1 END " + parameter; + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeTvShow, GetField(FieldId, strType), parameter); + } + else if (strType == "episodes") + { + table = "episode_view"; + + if (m_field == FieldGenre) + query = negate + FormatLinkQuery("genre", "genre", MediaTypeTvShow, (table + ".idShow").c_str(), parameter); + else if (m_field == FieldTag) + query = negate + FormatLinkQuery("tag", "tag", MediaTypeTvShow, (table + ".idShow").c_str(), parameter); + else if (m_field == FieldDirector) + query = negate + FormatLinkQuery("director", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter); + else if (m_field == FieldActor) + query = negate + FormatLinkQuery("actor", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter); + else if (m_field == FieldWriter) + query = negate + FormatLinkQuery("writer", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter); + else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST)) + query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter; + else if (m_field == FieldStudio) + query = negate + FormatLinkQuery("studio", "studio", MediaTypeTvShow, (table + ".idShow").c_str(), parameter); + else if (m_field == FieldMPAA) + query = negate + " (" + GetField(m_field, strType) + parameter + ")"; + } + if (m_field == FieldVideoResolution) + query = table + ".idFile" + negate + GetVideoResolutionQuery(param); + else if (m_field == FieldAudioChannels) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND iAudioChannels " + parameter + ")"; + else if (m_field == FieldVideoCodec) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strVideoCodec " + parameter + ")"; + else if (m_field == FieldAudioCodec) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strAudioCodec " + parameter + ")"; + else if (m_field == FieldAudioLanguage) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strAudioLanguage " + parameter + ")"; + else if (m_field == FieldSubtitleLanguage) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strSubtitleLanguage " + parameter + ")"; + else if (m_field == FieldVideoAspectRatio) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND fVideoAspect " + parameter + ")"; + else if (m_field == FieldAudioCount) + query = db.PrepareSQL(negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND streamdetails.iStreamtype = %i GROUP BY streamdetails.idFile HAVING COUNT(streamdetails.iStreamType) " + parameter + ")",CStreamDetail::AUDIO); + else if (m_field == FieldSubtitleCount) + query = db.PrepareSQL(negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND streamdetails.iStreamType = %i GROUP BY streamdetails.idFile HAVING COUNT(streamdetails.iStreamType) " + parameter + ")",CStreamDetail::SUBTITLE); + else if (m_field == FieldHdrType) + query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strHdrType " + parameter + ")"; + if (m_field == FieldPlaycount && strType != "songs" && strType != "albums" && strType != "tvshows") + { // playcount IS stored as NULL OR number IN video db + if ((m_operator == OPERATOR_EQUALS && param == "0") || + (m_operator == OPERATOR_DOES_NOT_EQUAL && param != "0") || + (m_operator == OPERATOR_LESS_THAN)) + { + std::string field = GetField(FieldPlaycount, strType); + query = field + " IS NULL OR " + field + parameter; + } + } + if (query.empty()) + query = CDatabaseQueryRule::FormatWhereClause(negate, oper, param, db, strType); + return query; +} + +std::string CSmartPlaylistRule::GetField(int field, const std::string &type) const +{ + if (field >= FieldUnknown && field < FieldMax) + return DatabaseUtils::GetField((Field)field, CMediaTypes::FromString(type), DatabaseQueryPartWhere); + return ""; +} + +std::string CSmartPlaylistRuleCombination::GetWhereClause(const CDatabase &db, const std::string& strType, std::set<std::string> &referencedPlaylists) const +{ + std::string rule; + + // translate the combinations into SQL + for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin(); it != m_combinations.end(); ++it) + { + if (it != m_combinations.begin()) + rule += m_type == CombinationAnd ? " AND " : " OR "; + std::shared_ptr<CSmartPlaylistRuleCombination> combo = std::static_pointer_cast<CSmartPlaylistRuleCombination>(*it); + if (combo) + rule += "(" + combo->GetWhereClause(db, strType, referencedPlaylists) + ")"; + } + + // translate the rules into SQL + for (CDatabaseQueryRules::const_iterator it = m_rules.begin(); it != m_rules.end(); ++it) + { + // don't include playlists that are meant to be displayed + // as a virtual folders in the SQL WHERE clause + if ((*it)->m_field == FieldVirtualFolder) + continue; + + if (!rule.empty()) + rule += m_type == CombinationAnd ? " AND " : " OR "; + rule += "("; + std::string currentRule; + if ((*it)->m_field == FieldPlaylist) + { + std::string playlistFile = CSmartPlaylistDirectory::GetPlaylistByName((*it)->m_parameter.at(0), strType); + if (!playlistFile.empty() && referencedPlaylists.find(playlistFile) == referencedPlaylists.end()) + { + referencedPlaylists.insert(playlistFile); + CSmartPlaylist playlist; + if (playlist.Load(playlistFile)) + { + std::string playlistQuery; + // only playlists of same type will be part of the query + if (playlist.GetType() == strType || (playlist.GetType() == "mixed" && (strType == "songs" || strType == "musicvideos")) || playlist.GetType().empty()) + { + playlist.SetType(strType); + playlistQuery = playlist.GetWhereClause(db, referencedPlaylists); + } + if (playlist.GetType() == strType) + { + if ((*it)->m_operator == CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL) + currentRule = StringUtils::Format("NOT ({})", playlistQuery); + else + currentRule = playlistQuery; + } + } + } + } + else + currentRule = (*it)->GetWhereClause(db, strType); + // if we don't get a rule, we add '1' or '0' so the query is still valid and doesn't fail + if (currentRule.empty()) + currentRule = m_type == CombinationAnd ? "'1'" : "'0'"; + rule += currentRule; + rule += ")"; + } + + return rule; +} + +void CSmartPlaylistRuleCombination::GetVirtualFolders(const std::string& strType, std::vector<std::string> &virtualFolders) const +{ + for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin(); it != m_combinations.end(); ++it) + { + std::shared_ptr<CSmartPlaylistRuleCombination> combo = std::static_pointer_cast<CSmartPlaylistRuleCombination>(*it); + if (combo) + combo->GetVirtualFolders(strType, virtualFolders); + } + + for (CDatabaseQueryRules::const_iterator it = m_rules.begin(); it != m_rules.end(); ++it) + { + if (((*it)->m_field != FieldVirtualFolder && (*it)->m_field != FieldPlaylist) || (*it)->m_operator != CDatabaseQueryRule::OPERATOR_EQUALS) + continue; + + std::string playlistFile = CSmartPlaylistDirectory::GetPlaylistByName((*it)->m_parameter.at(0), strType); + if (playlistFile.empty()) + continue; + + if ((*it)->m_field == FieldVirtualFolder) + virtualFolders.push_back(playlistFile); + else + { + // look for any virtual folders in the expanded playlists + CSmartPlaylist playlist; + if (!playlist.Load(playlistFile)) + continue; + + if (CSmartPlaylist::CheckTypeCompatibility(playlist.GetType(), strType)) + playlist.GetVirtualFolders(virtualFolders); + } + } +} + +void CSmartPlaylistRuleCombination::AddRule(const CSmartPlaylistRule &rule) +{ + std::shared_ptr<CSmartPlaylistRule> ptr(new CSmartPlaylistRule(rule)); + m_rules.push_back(ptr); +} + +CSmartPlaylist::CSmartPlaylist() +{ + Reset(); +} + +bool CSmartPlaylist::OpenAndReadName(const CURL &url) +{ + if (readNameFromPath(url) == NULL) + return false; + + return !m_playlistName.empty(); +} + +const TiXmlNode* CSmartPlaylist::readName(const TiXmlNode *root) +{ + if (root == NULL) + return NULL; + + const TiXmlElement *rootElem = root->ToElement(); + if (rootElem == NULL) + return NULL; + + if (!StringUtils::EqualsNoCase(root->Value(), "smartplaylist")) + { + CLog::Log(LOGERROR, "Error loading Smart playlist"); + return NULL; + } + + // load the playlist type + const char* type = rootElem->Attribute("type"); + if (type) + m_playlistType = type; + // backward compatibility: + if (m_playlistType == "music") + m_playlistType = "songs"; + if (m_playlistType == "video") + m_playlistType = "musicvideos"; + + // load the playlist name + XMLUtils::GetString(root, "name", m_playlistName); + + return root; +} + +const TiXmlNode* CSmartPlaylist::readNameFromPath(const CURL &url) +{ + CFileStream file; + if (!file.Open(url)) + { + CLog::Log(LOGERROR, "Error loading Smart playlist {} (failed to read file)", url.GetRedacted()); + return NULL; + } + + m_xmlDoc.Clear(); + file >> m_xmlDoc; + + const TiXmlNode *root = readName(m_xmlDoc.RootElement()); + if (m_playlistName.empty()) + { + m_playlistName = CUtil::GetTitleFromPath(url.Get()); + if (URIUtils::HasExtension(m_playlistName, ".xsp")) + URIUtils::RemoveExtension(m_playlistName); + } + + return root; +} + +const TiXmlNode* CSmartPlaylist::readNameFromXml(const std::string &xml) +{ + if (xml.empty()) + { + CLog::Log(LOGERROR, "Error loading empty Smart playlist"); + return NULL; + } + + m_xmlDoc.Clear(); + if (!m_xmlDoc.Parse(xml)) + { + CLog::Log(LOGERROR, "Error loading Smart playlist (failed to parse xml: {})", + m_xmlDoc.ErrorDesc()); + return NULL; + } + + const TiXmlNode *root = readName(m_xmlDoc.RootElement()); + + return root; +} + +bool CSmartPlaylist::load(const TiXmlNode *root) +{ + if (root == NULL) + return false; + + return LoadFromXML(root); +} + +bool CSmartPlaylist::Load(const CURL &url) +{ + return load(readNameFromPath(url)); +} + +bool CSmartPlaylist::Load(const std::string &path) +{ + const CURL pathToUrl(path); + return load(readNameFromPath(pathToUrl)); +} + +bool CSmartPlaylist::Load(const CVariant &obj) +{ + if (!obj.isObject()) + return false; + + // load the playlist type + if (obj.isMember("type") && obj["type"].isString()) + m_playlistType = obj["type"].asString(); + + // backward compatibility + if (m_playlistType == "music") + m_playlistType = "songs"; + if (m_playlistType == "video") + m_playlistType = "musicvideos"; + + // load the playlist name + if (obj.isMember("name") && obj["name"].isString()) + m_playlistName = obj["name"].asString(); + + if (obj.isMember("rules")) + m_ruleCombination.Load(obj["rules"], this); + + if (obj.isMember("group") && obj["group"].isMember("type") && obj["group"]["type"].isString()) + { + m_group = obj["group"]["type"].asString(); + if (obj["group"].isMember("mixed") && obj["group"]["mixed"].isBoolean()) + m_groupMixed = obj["group"]["mixed"].asBoolean(); + } + + // now any limits + if (obj.isMember("limit") && (obj["limit"].isInteger() || obj["limit"].isUnsignedInteger()) && obj["limit"].asUnsignedInteger() > 0) + m_limit = (unsigned int)obj["limit"].asUnsignedInteger(); + + // and order + if (obj.isMember("order") && obj["order"].isMember("method") && obj["order"]["method"].isString()) + { + const CVariant &order = obj["order"]; + if (order.isMember("direction") && order["direction"].isString()) + m_orderDirection = StringUtils::EqualsNoCase(order["direction"].asString(), "ascending") ? SortOrderAscending : SortOrderDescending; + + if (order.isMember("ignorefolders") && obj["ignorefolders"].isBoolean()) + m_orderAttributes = obj["ignorefolders"].asBoolean() ? SortAttributeIgnoreFolders : SortAttributeNone; + + m_orderField = CSmartPlaylistRule::TranslateOrder(obj["order"]["method"].asString().c_str()); + } + + return true; +} + +bool CSmartPlaylist::LoadFromXml(const std::string &xml) +{ + return load(readNameFromXml(xml)); +} + +bool CSmartPlaylist::LoadFromXML(const TiXmlNode *root, const std::string &encoding) +{ + if (!root) + return false; + + std::string tmp; + if (XMLUtils::GetString(root, "match", tmp)) + m_ruleCombination.SetType(StringUtils::EqualsNoCase(tmp, "all") ? CSmartPlaylistRuleCombination::CombinationAnd : CSmartPlaylistRuleCombination::CombinationOr); + + // now the rules + const TiXmlNode *ruleNode = root->FirstChild("rule"); + while (ruleNode) + { + CSmartPlaylistRule rule; + if (rule.Load(ruleNode, encoding)) + m_ruleCombination.AddRule(rule); + + ruleNode = ruleNode->NextSibling("rule"); + } + + const TiXmlElement *groupElement = root->FirstChildElement("group"); + if (groupElement != NULL && groupElement->FirstChild() != NULL) + { + m_group = groupElement->FirstChild()->ValueStr(); + const char* mixed = groupElement->Attribute("mixed"); + m_groupMixed = mixed != NULL && StringUtils::EqualsNoCase(mixed, "true"); + } + + // now any limits + // format is <limit>25</limit> + XMLUtils::GetUInt(root, "limit", m_limit); + + // and order + // format is <order direction="ascending">field</order> + const TiXmlElement *order = root->FirstChildElement("order"); + if (order && order->FirstChild()) + { + const char *direction = order->Attribute("direction"); + if (direction) + m_orderDirection = StringUtils::EqualsNoCase(direction, "ascending") ? SortOrderAscending : SortOrderDescending; + + const char *ignorefolders = order->Attribute("ignorefolders"); + if (ignorefolders != NULL) + m_orderAttributes = StringUtils::EqualsNoCase(ignorefolders, "true") ? SortAttributeIgnoreFolders : SortAttributeNone; + + m_orderField = CSmartPlaylistRule::TranslateOrder(order->FirstChild()->Value()); + } + return true; +} + +bool CSmartPlaylist::LoadFromJson(const std::string &json) +{ + if (json.empty()) + return false; + + CVariant obj; + if (!CJSONVariantParser::Parse(json, obj)) + return false; + + return Load(obj); +} + +bool CSmartPlaylist::Save(const std::string &path) const +{ + CXBMCTinyXML doc; + TiXmlDeclaration decl("1.0", "UTF-8", "yes"); + doc.InsertEndChild(decl); + + TiXmlElement xmlRootElement("smartplaylist"); + xmlRootElement.SetAttribute("type",m_playlistType.c_str()); + TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement); + if (!pRoot) + return false; + + // add the <name> tag + XMLUtils::SetString(pRoot, "name", m_playlistName); + + // add the <match> tag + XMLUtils::SetString(pRoot, "match", m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationAnd ? "all" : "one"); + + // add <rule> tags + m_ruleCombination.Save(pRoot); + + // add <group> tag if necessary + if (!m_group.empty()) + { + TiXmlElement nodeGroup("group"); + if (m_groupMixed) + nodeGroup.SetAttribute("mixed", "true"); + TiXmlText group(m_group.c_str()); + nodeGroup.InsertEndChild(group); + pRoot->InsertEndChild(nodeGroup); + } + + // add <limit> tag + if (m_limit) + XMLUtils::SetInt(pRoot, "limit", m_limit); + + // add <order> tag + if (m_orderField != SortByNone) + { + TiXmlText order(CSmartPlaylistRule::TranslateOrder(m_orderField).c_str()); + TiXmlElement nodeOrder("order"); + nodeOrder.SetAttribute("direction", m_orderDirection == SortOrderDescending ? "descending" : "ascending"); + if (m_orderAttributes & SortAttributeIgnoreFolders) + nodeOrder.SetAttribute("ignorefolders", "true"); + nodeOrder.InsertEndChild(order); + pRoot->InsertEndChild(nodeOrder); + } + return doc.SaveFile(path); +} + +bool CSmartPlaylist::Save(CVariant &obj, bool full /* = true */) const +{ + if (obj.type() == CVariant::VariantTypeConstNull) + return false; + + obj.clear(); + // add "type" + obj["type"] = m_playlistType; + + // add "rules" + CVariant rulesObj = CVariant(CVariant::VariantTypeObject); + if (m_ruleCombination.Save(rulesObj)) + obj["rules"] = rulesObj; + + // add "group" + if (!m_group.empty()) + { + obj["group"]["type"] = m_group; + obj["group"]["mixed"] = m_groupMixed; + } + + // add "limit" + if (full && m_limit) + obj["limit"] = m_limit; + + // add "order" + if (full && m_orderField != SortByNone) + { + obj["order"] = CVariant(CVariant::VariantTypeObject); + obj["order"]["method"] = CSmartPlaylistRule::TranslateOrder(m_orderField); + obj["order"]["direction"] = m_orderDirection == SortOrderDescending ? "descending" : "ascending"; + obj["order"]["ignorefolders"] = (m_orderAttributes & SortAttributeIgnoreFolders); + } + + return true; +} + +bool CSmartPlaylist::SaveAsJson(std::string &json, bool full /* = true */) const +{ + CVariant xsp(CVariant::VariantTypeObject); + if (!Save(xsp, full)) + return false; + + return CJSONVariantWriter::Write(xsp, json, true) && !json.empty(); +} + +void CSmartPlaylist::Reset() +{ + m_ruleCombination.clear(); + m_limit = 0; + m_orderField = SortByNone; + m_orderDirection = SortOrderNone; + m_orderAttributes = SortAttributeNone; + m_playlistType = "songs"; // sane default + m_group.clear(); + m_groupMixed = false; +} + +void CSmartPlaylist::SetName(const std::string &name) +{ + m_playlistName = name; +} + +void CSmartPlaylist::SetType(const std::string &type) +{ + m_playlistType = type; +} + +bool CSmartPlaylist::IsVideoType() const +{ + return IsVideoType(m_playlistType); +} + +bool CSmartPlaylist::IsMusicType() const +{ + return IsMusicType(m_playlistType); +} + +bool CSmartPlaylist::IsVideoType(const std::string &type) +{ + return type == "movies" || type == "tvshows" || type == "episodes" || + type == "musicvideos" || type == "mixed"; +} + +bool CSmartPlaylist::IsMusicType(const std::string &type) +{ + return type == "artists" || type == "albums" || + type == "songs" || type == "mixed"; +} + +std::string CSmartPlaylist::GetWhereClause(const CDatabase &db, std::set<std::string> &referencedPlaylists) const +{ + return m_ruleCombination.GetWhereClause(db, GetType(), referencedPlaylists); +} + +void CSmartPlaylist::GetVirtualFolders(std::vector<std::string> &virtualFolders) const +{ + m_ruleCombination.GetVirtualFolders(GetType(), virtualFolders); +} + +std::string CSmartPlaylist::GetSaveLocation() const +{ + if (m_playlistType == "mixed") + return "mixed"; + if (IsMusicType()) + return "music"; + // all others are video + return "video"; +} + +void CSmartPlaylist::GetAvailableFields(const std::string &type, std::vector<std::string> &fieldList) +{ + std::vector<Field> typeFields = CSmartPlaylistRule::GetFields(type); + for (std::vector<Field>::const_iterator field = typeFields.begin(); field != typeFields.end(); ++field) + { + for (const translateField& i : fields) + { + if (*field == i.field) + fieldList.emplace_back(i.string); + } + } +} + +bool CSmartPlaylist::IsEmpty(bool ignoreSortAndLimit /* = true */) const +{ + bool empty = m_ruleCombination.empty(); + if (empty && !ignoreSortAndLimit) + empty = m_limit <= 0 && m_orderField == SortByNone && m_orderDirection == SortOrderNone; + + return empty; +} + +bool CSmartPlaylist::CheckTypeCompatibility(const std::string &typeLeft, const std::string &typeRight) +{ + if (typeLeft == typeRight) + return true; + + if (typeLeft == "mixed" && + (typeRight == "songs" || typeRight == "musicvideos")) + return true; + + if (typeRight == "mixed" && + (typeLeft == "songs" || typeLeft == "musicvideos")) + return true; + + return false; +} + +CDatabaseQueryRule *CSmartPlaylist::CreateRule() const +{ + return new CSmartPlaylistRule(); +} +CDatabaseQueryRuleCombination *CSmartPlaylist::CreateCombination() const +{ + return new CSmartPlaylistRuleCombination(); +} |