# LazySearch2 plugin for SlimServer by Stuart Hickinbottom 2005
#
# $Id: LazySearch2.pm 163 2006-09-07 14:07:38Z stuarth $
#
# This code is derived from code with the following copyright message:
#
# SlimServer Copyright (c) 2001-2005 Sean Adams, Slim Devices Inc.
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2.
#
# 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.

# This is a plugin to implement lazy searching using the SqueezeBox remote
# control. Note that the SlimServer music database must be cleared and rebuilt
# after installing this plugin.
#
# For further details see:
# http://hickinbottom.demon.co.uk/SlimServer/lazy_searching2.htm

use strict;
use warnings;

package Plugins::LazySearch2;

use Slim::Utils::Strings qw (string);
use Slim::Utils::Misc;
use Slim::Utils::Text;
use Slim::Utils::Timers;
use Time::HiRes;

# Name of this plugin - used for various global things to prevent clashes with
# other plugins.
use constant PLUGIN_NAME => 'PLUGIN_LAZYSEARCH2';

# Name of the menu item that is expected to be added to the home menu.
use constant LAZYSEARCH_HOME_MENUITEM => 'LazySearch2';

# Mode for main lazy search mode and lazy search menu.
use constant LAZYSEARCH_TOP_MODE           => 'PLUGIN_LAZYSEARCH2.topmode';
use constant LAZYSEARCH_CATEGORY_MENU_MODE => 'PLUGIN_LAZYSEARCH2.categorymenu';
use constant LAZYBROWSE_MODE               => 'PLUGIN_LAZYSEARCH2.browsemode';

# Preference ranges and defaults.
use constant LAZYSEARCH_MINLENGTH_MIN            => 2;
use constant LAZYSEARCH_MINLENGTH_MAX            => 9;
use constant LAZYSEARCH_MINLENGTH_ARTIST_DEFAULT => 3;
use constant LAZYSEARCH_MINLENGTH_ALBUM_DEFAULT  => 3;
use constant LAZYSEARCH_MINLENGTH_GENRE_DEFAULT  => 3;
use constant LAZYSEARCH_MINLENGTH_TRACK_DEFAULT  => 4;
use constant LAZYSEARCH_LEFTDELETES_DEFAULT      => 1;
use constant LAZYSEARCH_HOOKSEARCHBUTTON_DEFAULT => 1;

# Constants that control the background lazy search database encoding.
use constant LAZYSEARCH_RESCAN_END_CHECK_FREQ => 10;
use constant LAZYSEARCH_ENCODE_MAX_QUANTA     => 0.5;

# Export the version to the server (as a subversion keyword).
use vars qw($VERSION);
$VERSION = 'branch-6_2-r163';

# The DataStore. We keep this as global to avoid keep looking it up.
my $ds;

# Whether the awful workaround for Bugzilla bug #2511/#2531 is necessary. See
# the use of this variable later for the gory details. If you don't use
# playlists or introduce new music through "browse music folders" then you
# should be able to set this to '0' to reduce the time taken for 'rescan'
# operations (perhaps preventing player stalls).
my $needArtistPlaylistBodge = 1;

# This hash-of-hashes contains state information on the current lazy search for
# each player. The first hash index is the player (eg $clientMode{$client}),
# and the second a 'parameter' for that player.
# The elements of the second hash are as follows:
#	search_items:	This holds the result of the find() operation, and will
#					be an array of album, artist or track objects.
#	search_text:	The current search text (ie the number keys on the remote
#					control).
#	search_performed:	A Boolean flag indicating whether a search has yet
#						been performed (and hence whether search_items has
#						the search results).
#	select_col:			This is the 'field' that the find returns.
#	search_col:			This is the column that the lazy search is applied to
#						when performing the find().
#	sortby_col:			The column used to order the result of the find().
#	player_title:		Start of line1 player text when there are search
#						results.
#	player_title_empty:	The line1 text when no search has yet been performed.
#	enter_more_prompt:	The line2 prompt shown when there is insufficient
#						search text entered to perform the search.
#	min_search_length:	The minimum number of characters that must be entered
#						before the lazy search is performed.
#	gettext:			Function reference to a method that can return the
#						next to display for a find-returned item.
#	onright:			Function reference to a method that enters a browse
#						mode on the item being displayed.
#	playlevel:			Identifying the type of item to be added to the
#						playlist if the user presses PLAY/ADD/INSERT on a
#						search result.
my %clientMode = ();

# Hash of outstanding database objects that are to be lazy-encoded. This is
# present to allow a background task to work on them in chunks, preventing
# performance problems caused by the server going busy for a long time.
# The structure of the hash is as follows:
# 	type => { lazify_sub => XX, ids => (YY) }
# Where:
# 	type is 'album', 'artist', 'genre' or 'track'.
# 	XX is a subroutine that will update the appropriate 'search' attribute of
# 		the object
# 	YY is a list (array) of IDs to process.
my %encodeQueues = ();

# Flag to protect against multiple initialisation or shutdown
my $initialised = 0;

# Flag to indicate whether we're currently applying 'lazification' to the
# database. Used to detect and warn the user of this when entering
# lazy search mode while this is in progress.
my $lazifying_database = 0;

# Map which is used to quickly translate the button pushes captured by our
# mode back into the numbers on those keys.
my %numberScrollMap = (
	'numberScroll_0' => '0',
	'numberScroll_1' => '1',
	'numberScroll_2' => '2',
	'numberScroll_3' => '3',
	'numberScroll_4' => '4',
	'numberScroll_5' => '5',
	'numberScroll_6' => '6',
	'numberScroll_7' => '7',
	'numberScroll_8' => '8',
	'numberScroll_9' => '9',
);

# Below are functions that are part of the standard SlimServer plugin
# interface.

# Main mode of this plugin; offers the artist/album/genre/song browse options
sub setMode {
	my $client = shift;
	my $method = shift || '';

	$::d_plugins
	  && Slim::Utils::Misc::msg("LazySearch2: setMode(method=$method)\n");

	# Handle request to exit our mode.
	if ( $method eq 'pop' ) {
		leaveMode($client);

		# Pop the current mode off the mode stack and restore the previous one
		Slim::Buttons::Common::popMode($client);
		return;
	}

	# Use INPUT.Choice to display the top-level search menu choices.
	my %params = (

		# The header (first line) to display whilst in this mode.
		header => '{LINE1_BROWSE} {count}',

		# A reference to the list of items to display.
		listRef => [qw({ARTISTS} {ALBUMS} {GENRES} {SONGS})],

		# A unique name for this mode that won't actually get displayed
		# anywhere.
		modeName => LAZYSEARCH_CATEGORY_MENU_MODE,

		# An anonymous function that is called every time the user presses the
		# RIGHT button.
		onRight => sub {
			my ( $client, $item ) = @_;

			# Search term initially empty.
			$clientMode{$client}{search_text}      = '';
			$clientMode{$client}{search_items}     = ();
			$clientMode{$client}{search_performed} = 0;

			if ( $item eq '{ARTISTS}' ) {
				$clientMode{$client}{select_col}   = 'artist';
				$clientMode{$client}{search_col}   = 'contributor.namesearch';
				$clientMode{$client}{sortby_col}   = 'artist';
				$clientMode{$client}{player_title} = '{LINE1_BROWSE_ARTISTS}';
				$clientMode{$client}{player_title_empty} =
				  '{LINE1_BROWSE_ARTISTS_EMPTY}';
				$clientMode{$client}{enter_more_prompt} =
				  'LINE2_ENTER_MORE_ARTISTS';
				$clientMode{$client}{min_search_length} =
				  Slim::Utils::Prefs::get(
					'plugin-lazysearch2-minlength-artist');
				$clientMode{$client}{gettext} = sub { return shift->name; };
				$clientMode{$client}{onright} = sub {
					my ( $client, $item ) = @_;

					# Browse albums by this artist.
					Slim::Buttons::Common::pushModeLeft(
						$client,
						'browsedb',
						{
							'hierarchy'    => 'artist,album,track',
							'level'        => 1,
							'findCriteria' => { 'artist' => $item->{'id'} },
						}
					);
				};
				$clientMode{$client}{playlevel} = 'artist';
				setSearchBrowseMode( $client, $item, 0 );
			} elsif ( $item eq '{ALBUMS}' ) {
				$clientMode{$client}{select_col}   = 'album';
				$clientMode{$client}{search_col}   = 'album.titlesearch';
				$clientMode{$client}{sortby_col}   = 'album';
				$clientMode{$client}{player_title} = '{LINE1_BROWSE_ALBUMS}';
				$clientMode{$client}{player_title_empty} =
				  '{LINE1_BROWSE_ALBUMS_EMPTY}';
				$clientMode{$client}{enter_more_prompt} =
				  'LINE2_ENTER_MORE_ALBUMS';
				$clientMode{$client}{min_search_length} =
				  Slim::Utils::Prefs::get('plugin-lazysearch2-minlength-album');
				$clientMode{$client}{gettext} = sub { return shift->title; };
				$clientMode{$client}{onright} = sub {
					my ( $client, $item ) = @_;

					# Browse tracks for this album.
					Slim::Buttons::Common::pushModeLeft(
						$client,
						'browsedb',
						{
							'hierarchy'    => 'track',
							'level'        => 0,
							'findCriteria' => { 'album' => $item->{'id'} },
						}
					);
				};
				$clientMode{$client}{playlevel} = 'album';
				setSearchBrowseMode( $client, $item, 0 );
			} elsif ( $item eq '{GENRES}' ) {
				$clientMode{$client}{select_col}   = 'genre';
				$clientMode{$client}{search_col}   = 'genre.namesearch';
				$clientMode{$client}{sortby_col}   = 'name';
				$clientMode{$client}{player_title} = '{LINE1_BROWSE_GENRES}';
				$clientMode{$client}{player_title_empty} =
				  '{LINE1_BROWSE_GENRES_EMPTY}';
				$clientMode{$client}{enter_more_prompt} =
				  'LINE2_ENTER_MORE_GENRES';
				$clientMode{$client}{min_search_length} =
				  Slim::Utils::Prefs::get('plugin-lazysearch2-minlength-genre');
				$clientMode{$client}{gettext} = sub { return shift->name; };
				$clientMode{$client}{onright} = sub {
					my ( $client, $item ) = @_;

					# Browse artists by this genre.
					Slim::Buttons::Common::pushModeLeft(
						$client,
						'browsedb',
						{
							'hierarchy'    => 'artist,album,track',
							'level'        => 0,
							'findCriteria' => { 'genre' => $item->{'id'} },
						}
					);
				};
				$clientMode{$client}{playlevel} = 'genre';
				setSearchBrowseMode( $client, $item, 0 );
			} elsif ( $item eq '{SONGS}' ) {
				$clientMode{$client}{select_col}   = 'track';
				$clientMode{$client}{search_col}   = 'track.titlesearch';
				$clientMode{$client}{sortby_col}   = 'title';
				$clientMode{$client}{player_title} = '{LINE1_BROWSE_TRACKS}';
				$clientMode{$client}{player_title_empty} =
				  '{LINE1_BROWSE_TRACKS_EMPTY}';
				$clientMode{$client}{enter_more_prompt} =
				  'LINE2_ENTER_MORE_TRACKS';
				$clientMode{$client}{min_search_length} =
				  Slim::Utils::Prefs::get('plugin-lazysearch2-minlength-track');
				$clientMode{$client}{gettext} = sub { return shift->title; };
				$clientMode{$client}{onright} = sub {
					my ( $client, $item ) = @_;

					# Push into the trackinfo mode for this one track.
					my $track = $ds->objectForId( 'track', $item->{'id'} );
					Slim::Buttons::Common::pushModeLeft( $client, 'trackinfo',
						{ 'track' => $track->url } );
				};
				$clientMode{$client}{playlevel} = 'track';
				setSearchBrowseMode( $client, $item, 0 );
			}

			# If rescan is in progress then warn the user.
			if ( $lazifying_database || Slim::Music::Import::stillScanning() ) {
				$::d_plugins
				  && Slim::Utils::Misc::msg(
					"LazySearch2: entering search while scan in progress\n");
				if ( $client->linesPerScreen == 1 ) {
					$client->showBriefly(
						{
							'line1' => $client->doubleString('SCAN_IN_PROGRESS')
						}
					);
				} else {
					$client->showBriefly(
						{ 'line1' => string('SCAN_IN_PROGRESS') } );
				}
			}
		},

		# These are all menu items and so have a right-arrow overlay
		overlayRef => sub {
			return [ undef, Slim::Display::Display::symbol('rightarrow') ];
		},
	);

	# Find the datastore we're going to do queries on later. This allows it
	# to change inbetween invocations of this mode (although I don't know why
	# it would change), without re-querying what it is excessively.
	$ds = Slim::Music::Info->getCurrentDataStore();

	# Use our INPUT.Choice-derived mode to show the menu and let it do all the
	# hard work of displaying the list, moving it up and down, etc, etc.
	if ( $method eq 'push' ) {
		Slim::Buttons::Common::pushModeLeft( $client,
			LAZYSEARCH_CATEGORY_MENU_MODE, \%params );
	} else {
		Slim::Buttons::Common::pushMode( $client, LAZYSEARCH_CATEGORY_MENU_MODE,
			\%params );
		$client->update();
	}
}

# Function called when leaving our top-level lazy search menu mode. We use this
# to track whether or not the user is within the lazy search mode for a
# particular player.
sub leaveMode {
	my $client = shift;

	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: leaving top-level lazy search menu mode\n");

	# Clear the search results to save a little memory.
	$clientMode{$client}{search_items} = ();
}

# There are no functions in this mode as the main mode (the top-level menu) is
# all handled by the INPUT.Choice mode.
sub getFunctions {
	return {};
}

# Return the name of this plugin; this goes on the server setting plugin
# page, for example.
sub getDisplayName {
	return PLUGIN_NAME;
}

# Set up this plugin when it's inserted or the server started. Adds our hooks
# for database encoding and makes our customised mode that lets us grab and
# process extra buttons.
sub initPlugin() {
	return if $initialised;    # don't need to do it twice

	$::d_plugins
	  && Slim::Utils::Misc::msg("LazySearch2: Initialising $VERSION\n");

	# Remember we're now initialised. This prevents multiple-initialisation,
	# which may otherwise cause trouble with duplicate hooks or modes.
	$initialised = 1;

	# Make sure the preferences are set to something sensible before we call
	# on them later.
	checkDefaults();

	# Add a command callback. This is used to detect and act upon database
	# rescans.
	Slim::Control::Command::setExecuteCallback(
		\&Plugins::LazySearch2::commandCallback );

	# Top-level menu mode. We register a custom INPUT.Choice mode so that
	# we can detect when we're in it (for SEARCH button toggle).
	Slim::Buttons::Common::addMode( LAZYSEARCH_TOP_MODE, undef, \&setMode );
	Slim::Buttons::Common::addMode(
		LAZYSEARCH_CATEGORY_MENU_MODE,
		Slim::Buttons::Input::Choice::getFunctions(),
		\&Slim::Buttons::Input::Choice::setMode
	);

	# Out input map for the new categories menu mode, based on thd default map
	# contents for INPUT.Choice.
	my %categoryInputMap = (
		'arrow_left'  => 'exit_left',
		'arrow_right' => 'exit_right',
		'play'        => 'play',
		'add'         => 'add',
		'stop'        => 'passback',
		'pause'       => 'passback',
	);
	for my $buttonPressMode (qw{repeat hold hold_release single double}) {
		$categoryInputMap{ 'play.' . $buttonPressMode }   = 'dead';
		$categoryInputMap{ 'add.' . $buttonPressMode }    = 'dead';
		$categoryInputMap{ 'search.' . $buttonPressMode } = 'dead';
		$categoryInputMap{ 'stop.' . $buttonPressMode }   = 'passback';
		$categoryInputMap{ 'pause.' . $buttonPressMode }  = 'passback';
	}
	Slim::Hardware::IR::addModeDefaultMapping( LAZYSEARCH_CATEGORY_MENU_MODE,
		\%categoryInputMap );

	# Make a customised version of the INPUT.Choice mode so that we can grab
	# the numbers (INPUT.Choice will normally use these to scroll through
	# 'numberScroll').
	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: making custom INPUT.Choice-derived modes\n");
	my %chFunctions = %{ Slim::Buttons::Input::Choice::getFunctions() };
	$chFunctions{'numberScroll'} = \&lazyKeyHandler;
	$chFunctions{'play'}         = \&onPlayHandler;
	$chFunctions{'addSingle'}    = \&onAddHandler;
	$chFunctions{'addHold'}      = \&onInsertHandler;
	$chFunctions{'leftSingle'}   = \&onDelCharHandler;
	$chFunctions{'leftHold'}     = \&onDelAllHandler;
	$chFunctions{'forceSearch'}  = \&lazyForceSearch;
	Slim::Buttons::Common::addMode( LAZYBROWSE_MODE, \%chFunctions,
		\&Slim::Buttons::Input::Choice::setMode );

	# Out input map for the new lazy browse mode, based on the default map
	# contents for INPUT.Choice.
	my %lazyInputMap = (
		'arrow_left'      => 'leftSingle',
		'arrow_left.hold' => 'leftHold',
		'arrow_right'     => 'exit_right',
		'play'            => 'play',
		'pause.single'    => 'pause',
		'pause.hold'      => 'stop',
		'add.single'      => 'addSingle',
		'add.hold'        => 'addHold',
		'search'          => 'forceSearch',
	);
	for my $buttonPressMode (qw{repeat hold hold_release single double}) {
		$lazyInputMap{ 'play.' . $buttonPressMode }   = 'dead';
		$lazyInputMap{ 'search.' . $buttonPressMode } = 'dead';
	}
	Slim::Hardware::IR::addModeDefaultMapping( LAZYBROWSE_MODE,
		\%lazyInputMap );

	# Intercept the 'search' button to take us to our top-level menu.
	Slim::Buttons::Common::setFunction( 'search', \&lazyOnSearch );

	# Check the database to ensure that the lazy search index has been
	# built. This is most useful on first install as it will perform the
	# initial database build.
	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: scheduling lazification on plugin initialisation\n");
	startScanEndMonitor();
}

sub shutdownPlugin() {
	return if !$initialised;    # don't need to do it twice

	$::d_plugins && Slim::Utils::Misc::msg("LazySearch2: Shutting down\n");

	# Remove the command callback we'd previously registered
	Slim::Control::Command::clearExecuteCallback(
		\&Plugins::LazySearch2::commandCallback );

	# @@TODO@@
	# Do we need to remove our top-level mode?

	# We're no longer initialised.
	$initialised = 0;
}

# Return information on this plugin's settings. The web interface will then
# present those on the 'server settings->plugins' page.
sub setupGroup {
	my %setupGroup = (
		PrefOrder => [
			'plugin-lazysearch2-minlength-artist',
			'plugin-lazysearch2-minlength-album',
			'plugin-lazysearch2-minlength-genre',
			'plugin-lazysearch2-minlength-track',
			'plugin-lazysearch2-leftdeletes',
			'plugin-lazysearch2-hooksearchbutton',
			'plugin-lazysearch2-lazifynow'
		],
		GroupHead         => string('SETUP_GROUP_PLUGIN_LAZYSEARCH2'),
		GroupDesc         => string('SETUP_GROUP_PLUGIN_LAZYSEARCH2_DESC'),
		GroupLine         => 1,
		GroupSub          => 1,
		Suppress_PrefSub  => 1,
		Suppress_PrefLine => 1,
	);

	my %setupPrefs = (
		'plugin-lazysearch2-minlength-artist' => {
			'validate'     => \&Slim::Web::Setup::validateInt,
			'validateArgs' =>
			  [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
			'PrefHead' => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST'),
			'PrefDesc' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_DESC'),
			'PrefChoose' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHOOSE'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHANGE'),
		},
		'plugin-lazysearch2-minlength-album' => {
			'validate'     => \&Slim::Web::Setup::validateInt,
			'validateArgs' =>
			  [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
			'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM'),
			'PrefChoose' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHOOSE'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHANGE'),
		},
		'plugin-lazysearch2-minlength-genre' => {
			'validate'     => \&Slim::Web::Setup::validateInt,
			'validateArgs' =>
			  [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
			'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE'),
			'PrefChoose' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHOOSE'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHANGE'),
		},
		'plugin-lazysearch2-minlength-track' => {
			'validate'     => \&Slim::Web::Setup::validateInt,
			'validateArgs' =>
			  [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
			'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK'),
			'PrefChoose' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHOOSE'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHANGE'),
		},
		'plugin-lazysearch2-leftdeletes' => {
			'validate'   => \&Slim::Web::Setup::validateTrueFalse,
			'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES'),
			'PrefDesc'   => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_DESC'),
			'PrefChoose' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHOOSE'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHANGE'),
			'options' => {
				'1' => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_1'),
				'0' => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_0')
			},
		},
		'plugin-lazysearch2-hooksearchbutton' => {
			'validate' => \&Slim::Web::Setup::validateTrueFalse,
			'PrefHead' => string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON'),
			'PrefDesc' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_DESC'),
			'PrefChoose' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHOOSE'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHANGE'),
			'options' => {
				'1' => string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_1'),
				'0' => string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_0')
			},
		},
		'plugin-lazysearch2-lazifynow' => {
			'validate'    => \&Slim::Web::Setup::validateAcceptAll,
			'PrefHead'    => string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW'),
			'PrefDesc'    => string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_DESC'),
			'changeIntro' =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_CHANGE'),
			'inputTemplate' => 'setup_input_submit.html',
			'ChangeButton'  =>
			  string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_BUTTON'),
			'onChange' => sub {
				if ( !$lazifying_database ) {
					$::d_plugins
					  && Slim::Utils::Misc::msg(
						"LazySearch2: manual lazification requested\n");
					startScanEndMonitor();
				}
			},
			'dontSet'   => 1,
			'changeMsg' => '',
		},
	);

	return ( \%setupGroup, \%setupPrefs );
}

# Below are functions that are specific to this plugin.

# Sub-mode, allowing entry search and browsing within a search category. This
# uses our custom mode to combine standard INPUT.Choice functionality with
# our handlers that catch the number keys (which would normally scroll the
# choices), and the play/add/insert buttons that we need so that we can
# manipulate the playlist as appropriate. This mode is driven through the
# clientMode hash (see its description at the head of this file).
sub setSearchBrowseMode {
	my $client = shift;
	my $method = shift;
	my $silent = shift;

	# Handle request to exit our mode.
	if ( $method eq 'pop' ) {
		$::d_plugins
		  && Slim::Utils::Misc::msg(
			"LazySearch2: popping out of searchBrowse mode\n");

		# Pop the current mode off the mode stack and restore the previous one
		Slim::Buttons::Common::popMode($client);
		return;
	}

	# The items for the list are those returned by the search (if there was
	# one), or the defined 'enter more' prompt if not.
	my $itemsRef;
	my $headerString;
	if ( ( length $clientMode{$client}{search_text} ) > 0 ) {
		$headerString =
		    $clientMode{$client}{player_title} . ' \''
		  . $clientMode{$client}{search_text} . '\'';
	} else {
		$headerString = $clientMode{$client}{player_title_empty};
	}

	# If we've actually performed a search then the title also includes
	# the item number/total items as per normal browse modes.
	if ( $clientMode{$client}{search_performed} ) {
		$itemsRef = $clientMode{$client}{search_items};
		$headerString .= ' {count}';
	} else {
		@$itemsRef =
		  ( $client->string( $clientMode{$client}{enter_more_prompt} ) );
	}

	# Parameters for our INPUT.Choice-derived mode.
	my %params = (

		# Text title on list1.
		header => $headerString,

		# A reference to the list of items to display.
		listRef => $itemsRef,

		# The function to extract the title of each item.
		name => \&lazyGetText,

		# Name for this mode.
		modeName => LAZYBROWSE_MODE,

		# Catch and handle the RIGHT button.
		onRight => \&lazyOnRight,

		# A handler that manages play/add/insert (differentiated by the
		# last parameter).
		onPlay => sub {
			my ( $client, $item, $addMode ) = @_;

			# Start playing the item selected (in the correct mode - play, add
			# or insert).
			lazyOnPlay( $client, $item, $addMode );
		},

		# These are all browsable items and so have a right-arrow overlay,
		# but if the list is empty then there is never an overlay.
		overlayRef => sub {
			my ( $client, $item ) = @_;
			my $listRef = $client->param('listRef');
			if ( !$clientMode{$client}{search_performed}
				|| ( scalar(@$listRef) == 0 ) )
			{
				return [ undef, undef ];
			} else {
				return [ undef, Slim::Display::Display::symbol('rightarrow') ];
			}
		},
	);

	# Use the new mode defined by INPUT.Choice and let it do all the hard work
	# of displaying the list, moving it up and down, etc, etc. We have a silent
	# version that doesn't scroll the mode in, which is used for subsequent
	# narrowing of the search (which will be replacing an already-displayed
	# version of this mode).
	if ($silent) {
		Slim::Buttons::Common::pushMode( $client, LAZYBROWSE_MODE, \%params );
	} else {
		Slim::Buttons::Common::pushModeLeft( $client, LAZYBROWSE_MODE,
			\%params );
	}
}

# Subroutine to extract the text to show for the browse/search. Most of this
# is stock here, we just need to defer to a specific function stored in the
# clientMode hash to get the actual text, as that differs for each item class.
sub lazyGetText {
	my ( $client, $item ) = @_;

	if ( !$clientMode{$client}{search_performed} ) {
		return $client->string( $clientMode{$client}{enter_more_prompt} );
	} else {
		my $listRef = $client->param('listRef');
		if ( scalar(@$listRef) == 0 ) {
			return $client->string('EMPTY');
		} else {
			my $getTextFunction = $clientMode{$client}{gettext};
			return &$getTextFunction($item);
		}
	}
}

# Make the SEARCH button force a search in the lazy search entry, consistent
# with the behaviour of the standard SEARCH button.
sub lazyForceSearch {
	my $client = shift;

	if ( !$clientMode{$client}{search_performed}
		&& ( length $clientMode{$client}{search_text} > 1 ) )
	{
		$::d_plugins
		  && Slim::Utils::Misc::msg("LazySearch2: forcing short text search\n");
		cancelPendingSearch($client);
		onFindTimer( 'dummy', $client, 1 );
	}
}

# Called when the user presses SEARCH. This allows toggling between the
# lazy search menu and the standard player search menu. A preference allows
# the SEARCH button to decide whether it's going to enter the standard or
# lazy search modes when it's currently in neither.
sub lazyOnSearch {
	my $client       = shift;
	my $mode         = Slim::Buttons::Common::mode($client);
	my $inLazySearch = ( $mode eq LAZYSEARCH_TOP_MODE )
	  || ( $mode eq LAZYSEARCH_CATEGORY_MENU_MODE )
	  || ( $mode eq LAZYBROWSE_MODE )
	  || 0;
	my $inSearch = 0;    #@@TODO@@@
	my $gotoLazy;
	if ( Slim::Utils::Prefs::get('plugin-lazysearch2-hooksearchbutton') ) {

		# Normal operation - enter lazy search as long as we're not already
		# in it, in which case we go to original search (allows double-search
		# to get back to the old mode).
		$gotoLazy = !$inLazySearch || 0;
	} elsif ($inSearch) {

		# If in original search mode we always enter lazy search.
		$gotoLazy = 1;
	} else {

		# Go into the standard search.
		$gotoLazy = 0;
	}

	$::d_plugins
	  && Slim::Utils::Misc::msg(
"LazySearch2: SEARCH button (mode='$mode', inSearch=$inSearch, inLazySearch=$inLazySearch, gotoLazy=$gotoLazy)\n"
	  );

	if ($gotoLazy) {

		# @@TODO@@
		# This doesn't seem to work properly - it doesn't seem to set the top
		# menu to the lazy item prior to jumping in. It seems to work,
		# but when existing the mode it doesn't exit to the lazy top
		# level item
		Slim::Buttons::Common::setMode( $client, 'home' );
		Slim::Buttons::Home::jump( $client, LAZYSEARCH_HOME_MENUITEM );
		Slim::Buttons::Common::pushMode( $client, LAZYSEARCH_TOP_MODE );
	} else {

		# Into the normal search menu.
		Slim::Buttons::Home::jumpToMenu( $client, "SEARCH" );
	}
}

# Subroutine to perform the 'browse into' RIGHT button handler for lazy search
# results. The browse mode just differs by the method used to start browsing
# for each type, and that's stored in the clientMode hash.
sub lazyOnRight {
	my ( $client, $item ) = @_;

	# If the list is empty then don't push into browse mode
	my $listRef = $client->param('listRef');
	if ( scalar(@$listRef) == 0 ) {
		$client->bumpRight();
	} else {

		# Only allow right if we've performed a search.
		if ( $clientMode{$client}{search_performed} ) {

			# Cancel any pending timer.
			cancelPendingSearch($client);

			# Push into the item details, using database browse.
			# The method executed is stored in the hash.
			my $onRightFunction = $clientMode{$client}{onright};
			return &$onRightFunction( $client, $item );
		}
	}
}

# Handle press of play/add/insert when on an item returned from the search.
# addMode=1 : add
# addMode=2 : insert
# addMode=3 : play
sub lazyOnPlay {
	my ( $client, $item, $addMode ) = @_;

	# Cancel any pending timer.
	cancelPendingSearch($client);

	# If no list loaded (eg search returned nothing), or
	# user has not entered enough text yet, then ignore the
	# command.
	my $listRef = $client->param('listRef');
	if ( !$clientMode{$client}{search_performed} ) {
		return;
	}

	my $id = $item->{'id'};
	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: play on track search (id $id), addMode=$addMode\n");
	my ( $line1, $line2, $msg, $cmd );

	if ( $addMode == 1 ) {
		$msg = "ADDING_TO_PLAYLIST";
		$cmd = "addtracks";
	} elsif ( $addMode == 2 ) {
		$msg = "INSERT_TO_PLAYLIST";
		$cmd = "inserttracks";
	} else {
		$msg =
		  Slim::Player::Playlist::shuffle($client)
		  ? "PLAYING_RANDOMLY_FROM"
		  : "NOW_PLAYING_FROM";
		$cmd = "loadtracks";
	}

	if ( $client->linesPerScreen == 1 ) {
		$line1 = $client->doubleString($msg);
	} else {
		$line1 = $client->string($msg);
		my $getTextFunction = $clientMode{$client}{gettext};
		$line2 = $item->{'name'};
	}
	$::d_plugins
	  && Slim::Utils::Misc::msg("LazySearch2: $msg ($line1, $line2)\n");
	$client->showBriefly(
		{
			'line1' => $line1,
			'line2' => $line2
		}
	);
	$client->execute(
		[
			'playlist', $cmd,
			sprintf( $clientMode{$client}{playlevel} . '=%d', $id )
		]
	);

	# Not sure why, but we don't need to start the play
	# here - seems something by default is grabbing and
	# processing the button. Strange...
}

# Pick up each number button press and add it to the current lazy search text,
# then re-search using that text.
sub lazyKeyHandler {
	my ( $client, $method ) = @_;

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	# Map the scroll number (the method invoked by the INPUT.Choice button
	# the lazy browse mode is based on), to a real number character.
	my $numberKey = $numberScrollMap{$method};
	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: lazy key entry, method=$method, number=$numberKey\n");
	$clientMode{$client}{search_text} .= $numberKey;

	# Update the display.
	updateLazyEntry( $client, $item );

	# Cancel any pending search and schedule another, so search happens
	# n seconds after the last button press.
	addPendingSearch($client);
}

# Update the display during lazy search entry. This is used on change of the
# lazy search text (ie add character or delete character).
sub updateLazyEntry {
	my ( $client, $item ) = @_;

	# Pop back into the mode to get the display updated. We ask for a 'silent'
	# update, which prevents the mode scrolling back in again.
	Slim::Buttons::Common::popMode($client);
	setSearchBrowseMode( $client, $item, 1 );
	$client->update();

	# In one-line display modes show a clue to the user, as he won't see the
	# result of the search for some time and won't otherwise get any visual
	# feedback.
	if ( $client->linesPerScreen == 1 ) {
		my $line =
		    $client->string('SHOWBRIEFLY_DISPLAY') . ' \''
		  . $clientMode{$client}{search_text} . '\'';
		$client->showBriefly( { 'line1' => $line } );
	}
}

# Schedule a new search to occur for the specified client.
sub addPendingSearch($) {
	my $client = shift;

	# Schedule a timer. Any existing one is cancelled first as we only allow
	# one outstanding one for this player.
	cancelPendingSearch($client);

	my $timerName = PLUGIN_NAME . Slim::Player::Client::id($client);
	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: adding timer called $timerName\n");
	Slim::Utils::Timers::setTimer( $timerName,
		Time::HiRes::time() + Slim::Utils::Prefs::get("displaytexttimeout"),
		\&onFindTimer, $client );
}

# Remove any outstanding lazy search timer. This is used when either leaving
# the search mode altogether, or when another key has been entered by the user
# (as a new later search will be scheduled instead).
sub cancelPendingSearch($) {
	my $client = shift;

	my $timerName = PLUGIN_NAME . Slim::Player::Client::id($client);
	$::d_plugins
	  && Slim::Utils::Misc::msg(
		"LazySearch2: cancelling timer called $timerName\n");

	# This seems tolerant of timers that don't exist, so no need to make sure
	# we actually have one scheduled.
	Slim::Utils::Timers::killOneTimer( $timerName, \&onFindTimer );
}

# Actually perform the lazy search and go back into the lazy search mode to
# get the results displayed.
sub onFindTimer() {
	my $timerName   = shift;
	my $client      = shift;
	my $forceSearch = shift || 0;

	$::d_plugins
	  && Slim::Utils::Misc::msg(
"LazySearch2: timer callback for $timerName (forceSearch=$forceSearch)\n"
	  );

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	# Perform lazy search, if a minimum length of search text is provided.
	my $itemsRef = $clientMode{$client}{search_items};
	if ( ( length $clientMode{$client}{search_text} ) >=
		$clientMode{$client}{min_search_length} || $forceSearch )
	{
		$client->showBriefly(
			{
				'line1' => sprintf(
					$client->string('LINE1_SEARCHING'),
					$clientMode{$client}{search_text}
				)
			}
		);
		my $findResults = $ds->find(
			{
				'field' => $clientMode{$client}{select_col},
				'find'  => {
					$clientMode{$client}{search_col} =>
					  buildFind( $clientMode{$client}{search_text} )
				},
				'sortBy' => $clientMode{$client}{sortby_col},
			}
		);

		# Each element of the listRef will be a hash with keys name and id.
		# This is true for artists, albums and tracks.
		my @searchItems = ();
		for my $findItem (@$findResults) {
			my $text = $clientMode{$client}{gettext}($findItem);
			my $id   = $findItem->id;
			push @searchItems, { name => $text, id => $id };
		}
		$clientMode{$client}{search_items}     = \@searchItems;
		$clientMode{$client}{search_performed} = 1;

		# Re-enter the search mode to get the display updated.
		Slim::Buttons::Common::popMode($client);
		setSearchBrowseMode( $client, $item, 1 );
		$client->update();
	} else {
		$clientMode{$client}{search_performed} = 0;
	}
}

# Construct the search terms. This takes into account the 'search substring'
# preference to build an appropriate array.
sub buildFind($) {
	my $searchText      = shift;
	my $searchSubstring = ( Slim::Utils::Prefs::get('searchSubString') );
	my $searchReturn;

	if ($searchSubstring) {
		$searchReturn = [ '%' . $searchText . '%' ];
	} else {

		# Search for start of words only. First word is found by tying it
		# to appearing immediately after the lazy version separator ('||'),
		# non-first words are found by making sure it appears after a space.
		$searchReturn = [
			'%' . lazyEncode(' ') . $searchText . '%',
			'%||' . $searchText . '%'
		];
	}

	return $searchReturn;
}

# Call the play/insert/add handler (passing the parameter to differentiate
# which function is actually needed).
sub onPlayHandler {
	my ( $client, $method ) = @_;
	my $onAdd = $client->param('onPlay');

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	&$onAdd( $client, $item, 0 );
}

# Call the play/insert/add handler (passing the parameter to differentiate
# which function is actually needed).
sub onAddHandler {
	my ( $client, $method ) = @_;
	my $onAdd = $client->param('onPlay');

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	&$onAdd( $client, $item, 1 );
}

# Call the play/insert/add handler (passing the parameter to differentiate
# which function is actually needed).
sub onInsertHandler {
	my ( $client, $method ) = @_;
	my $onAdd = $client->param('onPlay');

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	&$onAdd( $client, $item, 2 );
}

# Remove a single character from the search text. If this drops below the
# minimum the user is given the same prompts that he gets when he's entered
# less than the minimum search characters.
sub onDelCharHandler {
	my ( $client, $method ) = @_;

	$::d_plugins && Slim::Utils::Misc::msg("LazySearch2: del char called\n");

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	my $currentText = $clientMode{$client}{search_text};
	if ( ( length($currentText) > 0 )
		&& Slim::Utils::Prefs::get('plugin-lazysearch2-leftdeletes') )
	{

		# Remove the right-most character from the string.
		$clientMode{$client}{search_text} = substr( $currentText, 0, -1 );

		# 'cancel' a previous search if the length of the search text now
		# falls below the minimum.
		if ( ( length $clientMode{$client}{search_text} ) <
			$clientMode{$client}{min_search_length} )
		{
			$clientMode{$client}{search_performed} = 0;
		}

		# Update the display.
		updateLazyEntry( $client, $item );

		# Cancel any pending search and schedule another, so search happens
		# n seconds after the last button press.
		addPendingSearch($client);
	} else {

		# Clear the search results to save a little memory.
		$clientMode{$client}{search_items} = ();

		# Prevent any pending search timer from performing a search once
		# we've left this mode.
		cancelPendingSearch($client);

		# Search string is empty, so pop out.
		Slim::Buttons::Common::popModeRight($client);
	}
}

# Clear the current search and reset to the state you get into when no search
# text has yet been entered.
sub onDelAllHandler {
	my ( $client, $method ) = @_;

	$::d_plugins && Slim::Utils::Misc::msg("LazySearch2: del all called\n");

	my $listIndex = $client->param('listIndex');
	my $items     = $client->param('listRef');
	my $item      = $items->[$listIndex];

	my $currentText = $clientMode{$client}{search_text};
	if ( length($currentText) > 0 ) {

		# Reset the current search text.
		$clientMode{$client}{search_text} = '';

		# 'Cancel' any search that may have already been performed.
		$clientMode{$client}{search_performed} = 0;

		# Update the display.
		updateLazyEntry( $client, $item );
	}
}

# Called during initialisation, this makes sure that the plugin preferences
# stored are sensible. This has the effect of adding them the first time this
# plugin is activated and removing the need to check they're defined in each
# case of reading them.
sub checkDefaults {
	if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-artist') )
	{
		Slim::Utils::Prefs::set(
			'plugin-lazysearch2-minlength-artist',
			LAZYSEARCH_MINLENGTH_ARTIST_DEFAULT
		);
	}
	if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-album') )
	{
		Slim::Utils::Prefs::set(
			'plugin-lazysearch2-minlength-album',
			LAZYSEARCH_MINLENGTH_ALBUM_DEFAULT
		);
	}
	if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-genre') )
	{
		Slim::Utils::Prefs::set(
			'plugin-lazysearch2-minlength-genre',
			LAZYSEARCH_MINLENGTH_GENRE_DEFAULT
		);
	}
	if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-track') )
	{
		Slim::Utils::Prefs::set(
			'plugin-lazysearch2-minlength-track',
			LAZYSEARCH_MINLENGTH_TRACK_DEFAULT
		);
	}
	if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-leftdeletes') ) {
		Slim::Utils::Prefs::set( 'plugin-lazysearch2-leftdeletes',
			LAZYSEARCH_LEFTDELETES_DEFAULT );
	}
	if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-hooksearchbutton') )
	{
		Slim::Utils::Prefs::set(
			'plugin-lazysearch2-hooksearchbutton',
			LAZYSEARCH_HOOKSEARCHBUTTON_DEFAULT
		);
	}
}

# The SlimServer core calls back to this on any command. This function looks
# for database rescans (whether 'wipe' or just 'refresh' type), and if it
# sees such a command it initiates a background timer that periodically checks
# whether the scan is still underway.
sub commandCallback {

	# These are the two passed parameters
	my $client    = shift;
	my $paramsRef = shift;

	my $slimCommand = @$paramsRef[0];
	my $paramOne    = @$paramsRef[1];

	# Detect the start of a rescan. We then begin monitoring to detect when
	# it's finished.
	if ( ( $slimCommand eq 'rescan' ) || ( $slimCommand eq 'wipecache' ) ) {

		# A rescan has started. Begin monitoring for when that scan has ended
		# so that we can go through and add our lazy-search versions to the
		# database.
		$::d_plugins
		  && Slim::Utils::Misc::msg(
"LazySearch2: rescan detected; starting to monitor for scan completion\n"
		  );

		startScanEndMonitor();
	}

	# Bugzilla #2511/#2531.
	# DANGER WILL ROBINSON! This is a horrendous bodge, but because SlimServer
	# will think that any artist with a changed NAMESEARCH column is a new
	# artist during a rescan of playlists (and hence add it back, creating
	# apparent duplicates), we cheat here by spotting when that problem is
	# about to occur and reset the artists back to their 'non-lazy' forms.
	#
	# Like I say, this is ugly and may even create playback stalls if there
	# are a lot of artists (we can't break this up into task chunks because
	# we need to try to make sure it's complete before letting SlimServer start
	# the rescan).
	#
	# IT MAY NOT EVEN WORK, as command callbacks are called after the command
	# is executed. This may be OK in this case, though, as the scan is a long
	# task and we may be able to get in while the main part of the rescan is
	# being performed (ie the music), which doesn't cause the problem, before
	# the playlist part of the scan. If the user chooses to just rescan the
	# playlists, though, then we might not be so lucky, depending on how
	# the scanner works (if it adds a background task then I think that might
	# not get executed until after command callback is called).
	#
	# This ugly bodge (did I mention it's a bodge) can be removed if either:
	# 	a.	SlimServer starts looking for existing artists based on NAME rather
	# 		than NAMESEARCH in Contributor.pm.
	# 	b.	We get our own column in the database to store the lazy search
	# 		version in.
	# 	c.	Everyone stops using playlists (like me!).
	#
	# The 'end of scan' periodic check scheduled above will eventually put the
	# lazified versions of the artists back.
	#
	# This CERTAINLY WON'T FIX the problem of duplicate artists being created
	# by browsing into a folder with the new music in it (but you should be OK
	# if you bring that music in by a rescan instead).
	#
	# I don't like this; I feel dirty; but this is /slightly/ better than
	# nothing.
	if ( ( $slimCommand eq "rescan" ) && $needArtistPlaylistBodge ) {
		unlazifyArtists();
	}
}

# This starts a timer to monitor for the end of the core SlimServer database
# scan.
sub startScanEndMonitor() {
	my $timerName = PLUGIN_NAME . '.rescanCheck';

	# Remove any previous timer and add our new one.
	Slim::Utils::Timers::killTimers( $timerName, \&onRescanTimer );
	Slim::Utils::Timers::setTimer( $timerName,
		Time::HiRes::time() + LAZYSEARCH_RESCAN_END_CHECK_FREQ,
		\&onRescanTimer );
}

# This function is called periodically whilst a database scan is being
# performed (as detected through the command callback above). This checks
# whether the scan is still taking place, and if so schedules another timer
# in a while. If, however, the scan is found to have finished then that's the
# time to initiate the task of encoding each artist, track, genre and album
# into a lazy-searchable version.
sub onRescanTimer() {
	my $timerName = shift;
	my $client    = shift;

	# Check whether the scan is still going on; if so just schedule another
	# timer.
	if ( Slim::Music::Import::stillScanning() ) {
		my $newTimerName = PLUGIN_NAME . '.rescanCheck';
		$::d_plugins
		  && Slim::Utils::Misc::msg(
"LazySearch2: scan still in progress; deferring database lazification\n"
		  );
		Slim::Utils::Timers::setTimer( $newTimerName,
			Time::HiRes::time() + LAZYSEARCH_RESCAN_END_CHECK_FREQ,
			\&onRescanTimer );
	} else {
		$::d_plugins
		  && Slim::Utils::Misc::msg(
			"LazySearch2: scan finished; starting database lazification\n");
		lazifyDatabase();
	}
}

# This function is called when the music database scan has finished. It
# identifies each artist, track and album that has not yet been encoded into
# lazy form and schedules a SlimServer background task to encode them.
sub lazifyDatabase {

	# Convert the albums table.
	lazifyDatabaseType(
		'album',
		'album.titlesearch',
		sub {
			my $obj = shift;
			$obj->titlesearch( lazifyColumn( $obj->titlesearch ) );
		}
	);

	# Convert the artists (contributors) table.
	lazifyDatabaseType(
		'artist',
		'contributor.namesearch',
		sub {
			my $obj = shift;
			$obj->namesearch( lazifyColumn( $obj->namesearch ) );
		}
	);

	# Convert the genres table.
	lazifyDatabaseType(
		'genre',
		'genre.namesearch',
		sub {
			my $obj = shift;
			$obj->namesearch( lazifyColumn( $obj->namesearch ) );
		}
	);

	# Convert the songs (tracks) table.
	lazifyDatabaseType(
		'track',
		'track.titlesearch',
		sub {
			my $obj = shift;
			$obj->titlesearch( lazifyColumn( $obj->titlesearch ) );
		}
	);

	# If there are any items to encode then initialise a background task that
	# will do that work in chunks.
	if ( scalar keys %encodeQueues ) {
		Slim::Utils::Scheduler::add_task( \&encodeTask );
		$lazifying_database = 1;
	} else {
		$::d_plugins
		  && Slim::Utils::Misc::msg(
"LazySearch2: no object types require lazification - no task scheduled\n"
		  );
	}
}

# Return a lazy-encoded search column. This is a combination of the original
# search text (so that the original search function still works), combined with
# the lazy-encoded version. A separator is used between the two.
sub lazifyColumn {
	my $in = shift;
	my $out;

	# Some 'special' database entries don't have a search text version (I've
	# seen that with Various Artists). Leave those alone.
	if ( defined($in) && ( $in ne '' ) ) {
		$out = $in . '||' . lazyEncode($in);
	}

	return $out;
}

# This function examines the database for a specific 'object' type (artist,
# album or track), and looks for those which have not yet been lazy-encoded.
# Those it finds are added to a global hash that is later worked through from
# the background task.
sub lazifyDatabaseType {
	my $type       = shift;
	my $search_col = shift;
	my $lazify_sub = shift;
	$ds = Slim::Music::Info->getCurrentDataStore();

	# Find all items of the required type that have not yet been lazified.
	my %todoSet = map { $_ => 1 } $ds->find(
		{
			'field'  => $type,
			'idOnly' => 1,
			'find'   => { $search_col => { 'NOT LIKE' => '%||%' } }
		}
	);

	$::d_plugins
	  && Slim::Utils::Misc::msg(
		    "LazySearch2: lazify type=$type, search_col=$search_col todo="
		  . scalar( keys %todoSet )
		  . "\n" );

	# Put these IDs into the queue to be processed. Later, we'll work on these
	# in chunks from within a task.
	if ( scalar keys %todoSet ) {
		my %typeHash = ( lazify_sub => $lazify_sub, ids => [ keys %todoSet ] );
		$encodeQueues{$type} = \%typeHash;
	}
}

# Remove the lazification from artists. This is specifically present to work
# around the bug that causes artists to be duplicated within the database when
# there are playlists present or the user navigates into a music folder.
sub unlazifyArtists {
	$::d_plugins
	  && Slim::Utils::Misc::msg(
"LazySearch2: removing lazification of artists (Bugzilla #2511/#2531 partial workaround)\n"
	  );

	$ds = Slim::Music::Info->getCurrentDataStore();

	# Find all artists.
	my @allArtists = $ds->find( { 'field' => 'artist' } );
	for my $artist (@allArtists) {
		my $nameSearch = $artist->namesearch;
		if ( defined($nameSearch) ) {
			$nameSearch =~ s/\|\|.*$//;
			$artist->namesearch($nameSearch);
			$artist->update;
		}
	}

	$::d_plugins
	  && Slim::Utils::Misc::msg("LazySearch2: lazification removed\n");
}

# This task function is periodically called by SlimServer when it is 'idle'.
# It works through the IDs of the objects that require encoding. They are
# encoded in chunks taking a maximum amount of time to keep the server and
# players responsive. This function returns 0 when the task has finished, and
# 1 when there is more work to do and this function should be called again.
sub encodeTask {

	# Get a single type hash from the encode queue. It doesn't matter on the
	# order they come out of the hash.
	my $type        = ( keys %encodeQueues )[0];
	my $typeHashRef = $encodeQueues{$type};
	my %typeHash    = %$typeHashRef;

	# Go through and encode each of the identified IDs. To maintain performance
	# we will bail out if this takes more than a half a second.

	my $lazifySub  = $typeHash{lazify_sub};
	my $ids        = $typeHash{ids};
	my $start_time = Time::HiRes::time();
	$ds = Slim::Music::Info->getCurrentDataStore();

	$::d_plugins
	  && Slim::Utils::Misc::msg( "LazySearch2: encodeTask called - "
		  . scalar @$ids
		  . " $type object(s) remaining\n" );

	while (
		( scalar @$ids )
		&& ( ( Time::HiRes::time() - $start_time ) <
			LAZYSEARCH_ENCODE_MAX_QUANTA )
	  )
	{
		my $id = pop @$ids;

		# Update the search text for this one object and write it back to the
		# database. We check that the object was successfully retrived as it
		# may have been deleted in the meantime.
		my $obj = $ds->objectForId( $type, $id );
		if ( defined $obj ) {
			&$lazifySub($obj);
			$obj->update;
		}
	}

	# If we've exhausted the ids for this type then remove this type from the
	# hash. If there are any left, however, we'll leave those in for the task
	# next time.
	if ( !( scalar @$ids ) ) {
		delete $encodeQueues{$type};
		$::d_plugins
		  && Slim::Utils::Misc::msg("LazySearch2: exhaused IDs for $type\n");
	}

	# Find if there there is more work to do, and if so request that this task
	# is rescheduled.
	my $reschedule_task;
	if ( scalar keys %encodeQueues ) {
		$reschedule_task = 1;
	} else {
		$reschedule_task = 0;

		# Clear the global flag indicating the task is in progress.
		$lazifying_database = 0;
	}

	return $reschedule_task;
}

# Convert a search string to a lazy-entry encoded search string. This includes
# both the original search term and a lazy-encoded version. Later, when
# searching, both are tried. The original search text is kept in upper-case
# and the lazy version is encoded as digits - the latter is because both the
# original and lazy encoded version is searched in case the user bothers to
# put the search string in properly and this minimises the chance of erroneous
# matching.
#
# called:
#   undef
sub lazyEncode($) {
	my $in_string = shift;
	my $out_string;

	# This translates each searchable character into the number of the key that
	# shares that letter on the remote. Thus, this tells us what keys the user
	# will enter if he doesn't bother to multi-tap to get at the later
	# characters. Note that space maps to zero.
	# We do all this on an upper case version, since upper case is all the user
	# can enter through the remote control.
	$out_string = uc $in_string;
	$out_string =~
tr/ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 /2223334445556667777888999912345678900/;

	return $out_string;
}

# Standard plugin function to return our message catalogue. Many thanks to the
# following for the translations:
# 	DE	Dieter (dieterp@patente.de)
# 	ES	NC)stor (nspedalieri@gmail.com)
sub strings {
	return '
PLUGIN_LAZYSEARCH2
	DE	Faulpelz-Suche
	EN	Lazy Search Music
	ES	BC:squeda Laxa de MC:sica
	DA	Lazy Search Music
	NL	Lazy Search Music
	
PLUGIN_LAZYSEARCH2_TOPMENU
	DE	Faulpelz-Suche
	EN	Lazy Search Music
	ES	BC:squeda Laxa de MC:sica
	DA	Lazy Search Music
	NL	Lazy Search Music
	
LINE1_BROWSE
	DE	Faulpelz-Suche
	EN	Lazy Search
	ES	BC:squeda Laxa
	DA	Lazy Search
	NL	Lazy Search
	
LINE1_SEARCHING
	DE	Suchen nach \'%s\' ...
	EN	Searching for \'%s\' ...
	ES	Buscando \'%s\' ...
	DA	SC8ger efter \'%s\' ...
	NL	Zoekend naar \'%s\' ...

SHOWBRIEFLY_DISPLAY
	DE	Faulpelz-Suche
	EN	Lazy Search
	ES	BC:squeda Laxa
	DA	Lazy Search
	NL	Lazy Search

LINE1_BROWSE_ARTISTS
	DE	Passende Interpreten
	EN	Artists Matching
	ES	Artistas Coincidentes
	DA	Kunstner sC8gning
	NL	Gevonden Artiesten

LINE1_BROWSE_ARTISTS_EMPTY
	DE	Faulpelz-Suche nach Interpreten
	EN	Lazy Search for Artists
	ES	BC:squeda Laxa de Artistas
	DA	Lazy Search efter kunstner
	NL	Lazy Search naar Artiesten

LINE1_BROWSE_ALBUMS
	DE	Passende Alben
	EN	Albums Matching
	ES	Clbumes Coincidentes
	DA	Matchende albums
	NL	Gevonden Albums

LINE1_BROWSE_ALBUMS_EMPTY
	DE	Faulpelz-Suche nach Alben
	EN	Lazy Search for Albums
	ES	BC:squeda Laxa de Clbumes
	DA	Lazy Search efter Album
	NL	Lazy Search naar Albums
	
LINE1_BROWSE_TRACKS
	DE	Passende Titel
	EN	Songs Matching
	ES	Canciones Coincidentes
	DA	Matchende sange
	NL	Gevonden Liedjes

LINE1_BROWSE_TRACKS_EMPTY
	DE	Faulpelz-Suche nach Titel
	EN	Lazy Search for Songs
	ES	BC:squeda Laxa de Canciones
	DA	Lazy Search efter sange
	NL	Lazy Search naar Liedjes

LINE1_BROWSE_GENRES
	DE	Passende Stilrichtungen
	EN	Genres Matching
	ES	GC)neros Coincidentes
	DA	Matchende genre
	NL	Gevonden Genres

LINE1_BROWSE_GENRES_EMPTY
	DE	Faulpelz-Suche nach Stilrichtungen
	EN	Lazy Search for Genres
	ES	BC:squeda Laxa de GC)neros
	DA	Lazy Search efter genre
	NL	Lazy Search naar Genres

LINE2_ENTER_MORE_ARTISTS
	DE	Interpret eingeben
	EN	Enter Artist Search
	ES	Ingresar BC:squeda de Artista
	DA	Indtast kunstner
	NL	Artiest zoekopdracht

LINE2_ENTER_MORE_ALBUMS
	DE	Album eingeben
	EN	Enter Album Search
	ES	Ingresar BC:squeda de Clbumes
	DA	Indtast album
	NL	Album zoekopdracht

LINE2_ENTER_MORE_TRACKS
	DE	Titel eingeben
	EN	Enter Song Search
	ES	Ingresar BC:squeda de Canciones
	DA	Indtast sang
	NL	Liedjes zoekopdracht

LINE2_ENTER_MORE_GENRES
	DE	Stilrichtung eingeben
	EN	Enter Genre Search
	ES	Ingresar BC:squeda de GC)neros
	DA	Indtast genre
	NL	Genre zoekopdracht

SETUP_GROUP_PLUGIN_LAZYSEARCH2
	DE	Faulpelz-Suche
	EN	Lazy Search
	ES	BC:squeda Laxa
	DA	Lazy Search
	NL	Lazy Search

SETUP_GROUP_PLUGIN_LAZYSEARCH2_DESC
	DE	Mit den unten angebenen Einstellungen kann definiert werden, wie sich die Player-OberflC$che der Faulpelz-Suche verhC$lt. Es wird empfohlen, den Plugin-MenC<punkt <i>Faulpelz-Suche</i> zum HauptmenC< des Players hinzuzufC<gen, um einen einfachen Zugriff auf die Funktionen dieses Plugins zu ermC6glichen (die Standard <i>SEARCH</i>-Taste auf der Fernbedienung ermC6glicht ebenfalls den Zugang zu dieser FunktionalitC$t).
	EN	The settings below control how the lazy searching player interface performs. It is suggested that the <i>Lazy Search Music</i> menu item from this plugin is added to a player\'s home menu to provide easy access to this plugin\'s functions (the standard remote <i>search</i> button will also access this functionality)
	ES	La configuraciC3n debajo controla cC3mo actC:a la interface de bC:squeda laxa del reproductor. Se sugiere que el item de menC: <i>BC:squeda Laxa de MC:sica</i> para este plugin se aC1ada al menC: inicial del reproductor para brindar un acceso fC!cil a las funciones del plugin (el botC3n <i>search</i> estC!ndar del control remoto tendrC! tambiC)n acceso a esta funcionalidad).
	DA	Indstillingen nedenfor styrer ydelsen af lazy search afspillerens interface. Det er anbefalet at <i>Lazy Search Music</i> menuen fra dette plugin bliver tilfC8jet til en afspiller\'s home menu for at give nem adgang til dette plugin\'s funktioner. (Standard sC8gefunktionen vil ogsC% give adgang til denne funktionalitet).
	NL	Met de instellingen hieronder kan aangegeven worden hoe de Lazy SEARCH spelerinterface zich gedraagd. Het wordt aanbevolen de <i>Lazy SEARCH Muziek</i> functie aan het hoofdmenu van de speler toe te voegen, om zo een eenvoudige toegang tot de Lazy SEARCH plugin te verkrijgen (de normale <i>ZOEKknop</i> zal tevens deze functionaliteit verkrijgen).

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST
	DE	MindestlC$nge fC<r die Suche nach Interpreten
	EN	Minimum Artist Search Length
	ES	MC-nima Longitud para BC:squeda de Artista
	DA	Minimum kunstnersC8ge lC&ngde
	NL	Minimum Artiest zoekopdracht lengte

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_DESC
	DE	Die Suche nach Interpreten, Alben, Stilrichtungen oder Titel mit einer zu kleinen Zahl von Zeichen ist nicht besonders sinnvoll, da sie zu viele Ergebnisse liefert. Um zu verhindern, dass eine Suche gestartet wird, bevor eine sinnvolle Anzahl von Zeichen eingeben wurde, ist eine Mindestzahl von Zeichen vorgegeben. Es gibt unterschiedliche Einstellungen fC<r Interpretennamen, Albumnamen und Liedertitel - sinnvolle Voreinstellungen sind 3 fC<r Interpreten und Alben und 4 fC<r Lieder.
	EN	Searching for artists, albums, genres or songs with a short number of characters isn\'t very useful as it will return so many results. To prevent a search being performed until a more useful number of characters have been entered a mininum number of characters is specified here. There are separate settings for artists and album names, genres and song titles - a setting of 3 for artists, albums and genres, and 4 for songs, is a useful default.
	ES	El buscar artistas, C!lbumes, gC)neros o canciones con muy pocos caracteres no es muy C:til, ya que retornarC! demasiados resultados. Para evitar que se efectC:e una bC:squeda hasta que se hayan ingresado mC!s caracteres, se especifica aquC- un nC:mero mC-nimo de ellos. Existen configuraciones individuales para bC:squeda por nombre de artistas, nombre de C!lbumes, y nombre de canciones - valores por defecto apropiados son 3 caracteres para artistas, C!lbumes y gC)neros, y 4 caracteres para canciones.
	DA	SC8gning efter kunstner, album, genre eller sang med et kort antal tegn er ikke brugbart i praksis, da det vil resultere i et stort antal resultater. For at undgC% at sC8gningen starter fC8r et mere brugbart antal tegn bliver tastet, kan et minimum antal tegn specificeres her. Der er separate indstillinger for kunstner og album navn, genre og sangtitel. - Det anbefales at bruge 3 for kunstner, album og genre, og 4 for sangtitel.
	NL	Zoekend naar artiest, albums, genres of liederen met te weinig tekens is niet heel nuttig omdat het veel resultaten zal opleveren. Om te voorkomen dat een zoektocht verricht wordt voordat een nuttiger aantal tekens is ingevoerd, wordt hier een mininum aantal tekens gespecificeerd. Er zijn afzonderlijke instellingen voor artiest en album naam, genres en lied titels - een instelling van 3 voor artiest, albums en genres en 4 voor liederen, is een nuttige standaardwaarde. 
	
SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHOOSE
	DE	MindestlC$nge fC<r die Suche nach Interpreten (2-9 Zeichen):
	EN	Minimum length for artist search (2-9 characters):
	ES	MC-nima longitud para bC:squeda de artista (2-9 caracteres):
	DA	Minimum lC&ngde for kunstner sC8gning (2-9 tegn):
	NL	De minimumlengte voor Artiest zoekopdracht (2-9 tekens): 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHANGE
	DE	MindestlC$nge fC<r die Suche nach Interpreten wurde geC$ndert in:
	EN	Minimum length for artist search changed to:
	ES	MC-nima longitud para bC:squeda de artista cambiC3 a:
	DA	Minimum lC&ngde for kunstner sC8gning C&ndret til:
	NL	De minimumlengte voor Artiest zoekopdracht is gewijzigd in: 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM
	DE	MindestlC$nge fC<r die Suche nach Alben
	EN	Minimum Album Search Length
	ES	MC-nima Longitud para BC:squeda de Clbum
	DA	Minimum lC&ngde for album sC8gning
	NL	Minimum Album zoekopdracht lengte

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHOOSE
	DE	MindestlC$nge fC<r die Suche nach Alben (2-9 Zeichen):
	EN	Minimum length for album search (2-9 characters):
	ES	MC-nima longitud para bC:squeda de C!lbum (2-9 caracteres):
	DA	Minimum lC&ngde for album sC8gning (2-9 tegn):
	NL	De minimumlengte voor Album zoekopdracht (2-9 tekens): 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHANGE
	DE	MindestlC$nge fC<r die Suche nach Alben wurde geC$ndert in:
	EN	Minimum length for album search changed to:
	ES	MC-nima longitud para bC:squeda de C!lbum cambiC3 a:
	DA	Minimum lC&ngde for album sC8gning rettet til:
	NL	De minimumlengte voor Album zoekopdracht is gewijzigd in: 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK
	DE	MindestlC$nge fC<r die Suche nach Titel
	EN	Minimum Song Search Length
	ES	MC-nima Longitud para BC:squeda de CanciC3n
	DA	Minimum lC&ngde for sang sC8gning
	NL	Minimum Liedjes zoekopdracht lengte

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHOOSE
	DE	MindestlC$nge fC<r die Suche nach Titel (2-9 Zeichen):
	EN	Minimum length for song search (2-9 characters):
	ES	MC-nima longitud para bC:squeda de canciC3n (2-9 caracteres):
	DA	Minimum lC&ngde for sang sC8gning (2-9 tegn):
	NL	De minimumlengte voor Liedjes zoekopdracht (2-9 tekens): 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHANGE
	DE	MindestlC$nge fC<r die Suche nach Titel wurde geC$ndert in:
	EN	Minimum length for song search changed to:
	ES	MC-nima longitud para bC:squeda de canciC3n cambiC3 a:
	DA	Minimum lC&ngde for sang sC8gning rettet til:
	NL	De minimumlengte voor Liedjes zoekopdracht is gewijzigd in: 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE
	DE	MindestlC$nge fC<r die Suche nach Stilrichtungen
	EN	Minimum Genre Search Length
	ES	MC-nima Longitud para BC:squeda de GC)nero
	DA	Minimum lC&ngde for sang sC8gning
	NL	Minimum Genre zoekopdracht lengte

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHOOSE
	DE	MindestlC$nge fC<r die Suche nach Stilrichtungen (2-9 Zeichen):
	EN	Minimum length for genre search (2-9 characters):
	ES	MC-nima longitud para bC:squeda de gC)nero (2-9 caracteres):
	DA	Minimum lC&ngde for genre sC8gning (2-9 tegn):
	NL	De minimumlengte voor Genre zoektocht (2-9 tekens): 

SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHANGE
	DE	MindestlC$nge fC<r die Suche nach Stilrichtungen wurde geC$ndert in:
	EN	Minimum length for genre search changed to:
	ES	MC-nima longitud para bC:squeda de gC)nero cambiC3 a:
	DA	Minimum lC&ngde for genre sC8gning rettet til:
	NL	De minimumlengte voor Genre zoektocht is gewijzigd in: 

SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES
	DE	Verhalten der LINKS-Taste
	EN	LEFT Button Behaviour
	ES	Comportamiento del BotC3n IZQUIERDA
	DA	VENSTRE-knap opfC8rsel
	NL	LINKERknop Gedrag

SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_DESC
	DE	Man kann einstellen, wie sich die LINKS-Taste auf der Fernbedienung bei der Eingabe von Suchtext verhC$lt. Mit der LINKS-Taste kann entweder das zuletzt eingegeben Zeichen gelC6scht werden (z.B. um einen Fehler zu korrigieren) oder der Suchmodus beendet werden.
	EN	You can choose how the LEFT button on the remote control behaves when entering search text. LEFT can either delete the last character entered (eg to correct a mistake), or can exit the search mode altogether.
	ES	Se puede elegir como se comportarC! el boton IZQUIERDA del control remoto cuando se ingresa texto. IZQUIERDA puede o bien borrar el C:ltimo caracter ingresado (por ej, para corregir un error), o bien puede abandonar el modo bC:squeda.
	DA	Du kan vC&lge hvordan VESTRE-knappen pC% fjernbetjC&ningen opfC8rer sig nC%r man trykker en sC8getekst. VESTRE kan enten slette det sidst tastet tegn (for at rette en fejl), eller forlade sC8geningen.
	NL	U kan kiezen hoe de LINKERknop op de afstandsbediening zich gedraagt tijdens het invoeren van een zoektekst. LINKERknop kan het laatst ingevoerde teken (om een fout te verbeteren) schrappen, of kan de zoek modus te verlaten. 

SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHOOSE
	DE	DrC<cken der LINKS-Taste wC$hrend einer Suche:
	EN	Pressing LEFT while entering a search:
	ES	Presionando IZQUIERDA mientras se ingresa una bC:squeda:
	DA	Ved tryk pC% VENSTRE under sC8gning:
	NL	LINKERknop gebruikt in zoekmode:

SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHANGE
	DE	DrC<cken der LINKS-Taste wurde geC$ndert in:
	EN	Pressing LEFT changed to:
	ES	Presionando IZQUIERDA cambiC3 a:
	DA	Tyk VENSTRE for at:
	NL	LINKERknop gewijzigd in:

SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_0
	DE	Beendet den Suchmodus
	EN	Exits the search mode
	ES	Abandona el modo bC:squeda
	DA	Forlade sC8gning
	NL	Verlaat de zoekmode

SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_1
	DE	LC6scht das zuletzt eingegebene Zeichen
	EN	Deletes the last character entered
	ES	Borra los C:ltimos caracteres ingresados
	DA	Slette det sidst tastede tegn
	NL	Verwijder het laatst ingevoerde karakter

SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON
	DE	Verhalten der SEARCH-Taste
	EN	SEARCH Button Behaviour
	ES	Comportamiento del BotC3n SEARCH
	DA	SEARCH-knap opfC8rsel
	NL	ZOEKknop Gedrag

SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_DESC
	DE	Mit dieser Einstellung kann die SEARCH-Taste auf der Squeezebox-Fernbedienung mit der <i>Faulpelz-Suche</i> statt mit der <i>Originalsuche</i> belegt werden. Durch Aktivieren dieser Einstellung kann diese Taste entsprechend umbelegt werden, ohne die Dateien <i>Default.map</i> oder <i>Custom.map</i> C$ndern zu mC<ssen. Hinweis: Cnderungen an dieser Einstellung werden erst nach einem erneuten Start des Plugins wirksam (z.B. bei einem Neustart des SlimServers).
	EN	This setting allows the SEARCH button on the Squeezebox remote to be remapped to the <i>lazy search music</i> function instead of the original <i>search music</i> function. Enabling this setting allows this button remapping to be performed without editing the <i>Default.map</i> or <i>Custom.map</i> files. Note that changes to this setting do not take effect until the plugin is reloaded (eg by restarting SlimServer).
	ES	Esta configuraciC3n permite reasignar el boton SEARCH del control remoto de Squeezebox a la funciC3n de <i>bC:squeda laxa de mC:sica</i>, en lugar de la funciC3n de <i>bC:squeda de mC:sica</i> original. Habilitando esto se logra que la reasignaciC3n del botC3n sea realizada sin editar los archivos <i>Default.map</i> o <i>Custom.map</i>. Notar que los cambios no tendrC!n efecto hasta que el plugin sea recargado (por ej. al reiniciar SlimServer).
	DA	Denne indstilling giver mulighed for at SEARCH knappen pC% Squeezebox/Transporter fjernbetjC&ningen benyttes til at aktivere <i>Lazy Search Music</i> funktionen i stedet for den orginale <i>sC8g</i> funktion. Det er ikke nC8dvendigt at rette i <i>Default.map</i> eller <i>Custom.map filerne. BemC&rk, denne indstilling slC%r ikke igennem fC8r plugin\'et er genindlC8st (f.eks. ved at genstarte SlimServer).
	NL	Met deze instelling kan de ZOEKknop op de Squeezebox afstandsbediening gewijzigd worden door <i>de Lazy SEARCH muziek</i> functie in plaats van het originele <i>Zoek muziek</i> functie. Door deze instelling te activeren deze ZOEKknop wijzigind doorgevoerd worden zonder wijziging van de <i>Default.map</i> of <i>Custom.map</i>. Merk op dat de veranderingen in van deze instelling niet van kracht wordt totdat de plugin wordt herladen (b.v. door SlimServer opnieuw te starten).

SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHOOSE
	DE	DrC<cken der SEARCH-Taste auf der Squeezebox-Fernbedienung:
	EN	Pressing SEARCH on the Squeezebox remote:
	ES	Presionando SEARCH en el remoto de Squeezebox:
	DA	Tryk pC% SEARCH knappen pC% Squeezebox/Transporter fjernbetjC&ningen:
	NL	ZOEKknop op de Squeezebox afstandsbediening:

SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHANGE
	DE	DrC<cken der SEARCH-Taste wurde geC$ndert in:
	EN	Pressing SEARCH changed to:
	ES	Presionando SEARCH cambiC3 a:
	DA	Tryk pC% SEARCH gC%r til:
	NL	ZOEKknop gewijzigd in:

SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_0
	DE	Zeigt das MenC< der Standardsuche an
	EN	Accesses the standard search music menu
	ES	Accede al menC: de bC:squeda musical estC!ndar
	DA	Standard sC8gning
	NL	Geeft toegang tot het normale zoek muziek menu

SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_1
	DE	Zeigt das MenC< der Faulpelz-Suche an
	EN	Accesses the lazy search music menu
	ES	Accede al menC: de bC:squeda musical laxa
	DA	Lazy Serach menuen.
	NL	Geeft toegang tot het Lazy Search muziek menu

SCAN_IN_PROGRESS
	DE	Hinweis: Die Musikdatenbank wird gerade durchsucht
	EN	Note: music library scan in progress
	ES	Nota: se estC! recopilando la colecciC3n musical
	DA	Note: dit musik biblioteket bliver lige nu scannet.
	NL	Merk op: muziek bibliotheek wordt nu ingelezen

SCAN_IN_PROGRESS_DBL
	DE	Hinweis: Suche lC$uft
	EN	Note: scanning
	ES	Nota: recopilando
	DA	Note: scanner
	NL	Merk op: Inlezen...

SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW
	DE	Indexerzeugung fC<r die Faulpelz-Suche
	EN	Force Lazy Search Index Build
	ES	Forzar CreaciC3n de Cndice para BC:squeda Laxa
	DA	Gennemtving opbygningen af Lazy Seach indexet
	NL	Forceer de Lazy Search Indexering

SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_DESC
	DE	Das Plugin erzeugt den Index fC<r die Faulpelz-Suche, wenn dies erforderlich ist. Normalerweise ist daher keine extra Pflege der Datenbank notwendig. Falls Sie sichergehen wollen, dass der Index der Faulpelz-Suche korrekt erzeugt wurde, kC6nnen Sie die folgende SchaltflC$che anklicken. Aber in Anbetracht dessen, dass dies nie erforderlich sein sollte, ist dies in erster Linie eine Hilfe fC<r die Fehlersuche.
	EN	The plugin is designed to build the lazy search index whenever required and so, under normal circumstances, no extra database maintenance is required. If you wish to ensure that the lazy search index has been correctly built you can press the following button, but given that it should never be necessary this is primarily a debugging aid.
	ES	El plugin se ha diseC1ado para construir el C-ndice de bC:squeda laxa cuando sea que se requiera. Por lo tanto, en circunstancias normales, no se requiere mantenimiento extra de la base de datos. Si se quiere estar seguro que el C-ndice de bC:squeda laxa ha sido construido correctamente, se puede presionar el siguiente botC3n (aunque dado que nunca deberC-a ser necesario reconstruirlo manualmente se lo incluye aquC- simplemente como una ayuda para la depuraciC3n).
	DA	Dette plugin er lavet til at vedligeholde Lazy Search indexet nC%r det er nC8dvendigt. Derfor er det ikke, under normale omstC&ndigheder, nC8dvendigt at tivnge re-index igennem. Du kan dog, hvis du vil vC&re sikker pC% at indexet er opbygget korrekt, trykke denne knap. Dette er primC&rt en debug funktion.
	NL	De plugin is zo ontworpen dat de Lazy Search index, wanneer het noodzakelijk is aangemaakt wordt, onder normale omstandigheden, is er geen extra  onderhoud vereist. Indien u er zeker van wilt zijn dat de Lazy Search index correct is ingelezen, kan u de volgende knop gebruiken, maar gegeven dat, het zal nooit noodzakelijk deze knop te moeten gebruiken, het is hoofdzakelijk een debugging tool. 

SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_CHANGE
	DE	Die Erzeugung des Index fC<r die Faulpelz-Suche hat begonnen
	EN	Lazy search index build has been started
	ES	La creaciC3n del C-ndice para bC:squeda laxa ha comenzado
	DA	Lazy Search indexet bliver du genopbygget
	NL	Lazy Search indexering is gestart

SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_BUTTON
	DE	Jetzt den Index fC<r die Faulpelz-Suche erzeugen
	EN	Build Lazy Search Index Now
	ES	Crear Cndice de BC:squeda Laxa Ahora
	DA	Start opbygning af Lazy Search indexet
	NL	CreC+er de Lazy Search Index
';
}

1;

__END__

# Local Variables:
# tab-width:4
# indent-tabs-mode:t
# End:

