Index: Slim/DataStores/DBI/Contributor.pm =================================================================== --- Slim/DataStores/DBI/Contributor.pm (revision 4416) +++ Slim/DataStores/DBI/Contributor.pm (working copy) @@ -90,7 +90,7 @@ my $sort = Slim::Utils::Text::ignoreCaseArticles(($sortedList[$i] || $name)); my $artistObj = Slim::DataStores::DBI::Contributor->find_or_create({ - namesearch => $search, + namesearch => Slim::DataStores::DBI::DataModel::preStoreSearchFilter(Slim::DataStores::DBI::DataModel::SEARCH_COLUMN_CONTRIBUTOR, $search), }); $artistObj->name($name); Index: Slim/DataStores/DBI/DataModel.pm =================================================================== --- Slim/DataStores/DBI/DataModel.pm (revision 4416) +++ Slim/DataStores/DBI/DataModel.pm (working copy) @@ -27,6 +27,47 @@ our $dirtyCount = 0; our $cleanupIds; +# Hash of encode hooks that allow the server or plugins to preprocess the +# search text before it is stored in the database. Each has entry has the +# The hash has the following structure: +# '$columnType' => { '$key' => \$hookFunctionRef } +# Where: +# $key is a string that is used to identify the owner of the hook. This is +# not processed, but is used to identify the hook so that it can be +# removed. This must be unique system-wide (recommendation is to +# include the plugin name to help with this). +# $columnType is a string identifying the search column type that is to +# be hooked. This can be one of: +# album +# track +# contributor +# genre +# $hookFunctionRef is a reference to a function that will process and encode +# the search string. This function has the following form: +# hookFunctionRef($type, $inString) { return $outString; } +# $type is passed in so that a plugin may handle all columns in one +# hook function if necessary, while still being able to encode column +# search text differently. +# $in_string is the original string that would normally undergo simple +# encoding before storage in the database - this simple encoding does +# not take place when a hook for the type is present. +# $out_string is an encoded form of the original string. This will be +# stored as-is within the database +# +# Note that it possible for multiple hooks to be present for the same column +# type. In this case the input string is run through each in an arbitrary +# order. +my %preStoreSearchFilterHooks = (); + +# These contants identify names for the colums in the datastore that have +# a 'searchable' version held. They are used inconjunction with the search +# encoding hooks, and should also be used by plugins rather than hardcoding +# the names. +use constant SEARCH_COLUMN_ALBUM => 'album'; +use constant SEARCH_COLUMN_TRACK => 'track'; +use constant SEARCH_COLUMN_CONTRIBUTOR => 'contributor'; +use constant SEARCH_COLUMN_GENRE => 'genre'; + { my $class = __PACKAGE__; @@ -913,6 +954,55 @@ return $class; } +# Add a new search text filter hook to be called before it is stored in the +# database. +# See description of preStoreSearchFilterHooks hash for further details. +sub addPreStoreSearchFilterHook($$$) { + my $type = shift; + my $key = shift; + my $hookFunctionRef = shift; + + $::d_sql && msg("Adding preStoreSearchFilterHook for $type=>$key\n"); + $preStoreSearchFilterHooks{$type}{$key} = $hookFunctionRef; +} + +# Remove a search text filter hook for a particular type. +# See description of preStoreSearchFilterHooks hash for further details. +sub removePreStoreSearchFilterHook($$) { + my $type = shift; + my $key = shift; + + $::d_sql && msg("Removing preStoreSearchFilterHook for $type=>$key\n"); + delete $preStoreSearchFilterHooks{$type}{$key}; +} + +# Call all defined filter hooks for a particular search text type. +# See description of preStoreSearchFilterHooks hash for further details. +sub preStoreSearchFilter($$) { + my $type = shift; + my $searchString = shift; + + # Any hooks defined fore this type? + if ( defined( $preStoreSearchFilterHooks{$type} ) ) { + + # Loop through the hooks for this type, allowing each to process the + # search string. + my $typeHooks = $preStoreSearchFilterHooks{$type}; + for my $key ( keys %$typeHooks ) { + my $hookRef = $preStoreSearchFilterHooks{$type}{$key}; + $searchString = &$hookRef( $type, $searchString ); + } + } + else { + + # Default behaviour if no specific hooks - make upper case and remove + # insignificant articles. + $searchString = Slim::Utils::Text::ignoreCaseArticles($searchString); + } + + return $searchString; +} + 1; __END__ Index: Slim/DataStores/DBI/DBIStore.pm =================================================================== --- Slim/DataStores/DBI/DBIStore.pm (revision 4416) +++ Slim/DataStores/DBI/DBIStore.pm (working copy) @@ -1341,7 +1341,7 @@ } # Create a canonical title to search against. - $attributes->{'TITLESEARCH'} = Slim::Utils::Text::ignoreCaseArticles($attributes->{'TITLE'}); + $attributes->{'TITLESEARCH'} = Slim::DataStores::DBI::DataModel::preStoreSearchFilter(Slim::DataStores::DBI::DataModel::SEARCH_COLUMN_TRACK, $attributes->{'TITLE'}); # Remote index. $attributes->{'REMOTE'} = Slim::Music::Info::isRemoteURL($url) ? 1 : 0; @@ -1557,7 +1557,7 @@ $albumObj->titlesort($sortable_title) if $sortable_title; # And our searchable version. - $albumObj->titlesearch(Slim::Utils::Text::ignoreCaseArticles($album)); + $albumObj->titlesearch(Slim::DataStores::DBI::DataModel::preStoreSearchFilter(Slim::DataStores::DBI::DataModel::SEARCH_COLUMN_ALBUM, $album)); $albumObj->compilation( $attributes->{'COMPILATION'} ? 1 : 0 ); Index: Slim/Buttons/Search.pm =================================================================== --- Slim/Buttons/Search.pm (revision 4416) +++ Slim/Buttons/Search.pm (working copy) @@ -12,6 +12,7 @@ use File::Spec::Functions qw(updir); use Slim::Buttons::Common; use Slim::Display::Display; +use Slim::Utils::Misc; # button functions for search directory my @defaultSearchChoices = qw(ARTISTS ALBUMS SONGS); @@ -21,6 +22,26 @@ our %context = (); our %menuParams = (); +# Hash of filter hooks that allow the server or plugins to preprocess the +# search term before it is used in a database query. +# following form: +# { '$key' => \$hookFunctionRef } +# Where: +# $key is a string that is used to identify the owner of the hook. This is +# not processed, but is used to identify the hook so that it can be +# removed. This must be unique system-wide (recommendation is to +# include the plugin name to help with this). +# $hookFunctionRef is a reference to a function that will process and filter +# the search term. This function has the following form: +# hookFunctionRef($client, $searchTerm) { modify @$searchTerm } +# $client is the client (player) performing the search +# $searchTerm is a reference to an array containing the search terms. +# This is a reference and so the array is modified 'by reference'. +# +# Note that it possible for multiple hooks to be present. In this case the +# search term is run through each in an arbitrary order. +my %searchTermFilterHooks = (); + sub init { my %subs = ( @@ -204,13 +225,57 @@ # If we're searching in substrings, return - otherwise append another # search which is effectively \b for the query. We might (should?) # deal with alternate separator characters other than space. + my @searchTerm; if (Slim::Utils::Prefs::get('searchSubString')) { - return [ $term ]; + @searchTerm = ( $term ); + } else { + @searchTerm = ( $term, "* $term" ); } - return [ $term, "* $term" ]; + # Allow the search term to be filtered if any hooks have been defined. + searchTermFilter($client, \@searchTerm); + return \@searchTerm; } +# Add a new filter hook to be called to allow the search term to be +# (potentially) modified. +# See description of searchTermFilterHooks hash for further details. +sub addSearchTermFilterHook($$) { + my $key = shift; + my $hookFunctionRef = shift; + + $::d_sql + && Slim::Utils::Misc::msg("Adding searchTermFilterHook for $key\n"); + $searchTermFilterHooks{$key} = $hookFunctionRef; +} + +# Remove a defined search term filter hook. +# See description of searchTermFilterHooks hash for further details. +sub removeSearchTermFilterHook($) { + my $key = shift; + + $::d_sql + && Slim::Utils::Misc::msg("Removing searchTermFiterHook for $key\n"); + delete $searchTermFilterHooks{$key}; +} + +# Call all defined filter hooks. +# See description of searchTermFilterHooks hash for further details. +sub searchTermFilter($$) { + my $client = shift; + my $searchTerm = shift; + + # Give each defined filter a chance to modify the search term. No filters + # mean that no modification happens. + for my $key ( keys %searchTermFilterHooks ) { + my $hookRef = $searchTermFilterHooks{$key}; + + $::d_sql + && Slim::Utils::Misc::msg("Calling searchTermFilterHook for $key\n"); + &$hookRef( $client, $searchTerm ); + } +} + 1; __END__