Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Ticket #52: LazySearch2.pm

File LazySearch2.pm, 70.0 KB (added by stuarth, 6 years ago)

6.2-compatible version of the plugin containing the Dutch strings to extract

Line 
1# LazySearch2 plugin for SlimServer by Stuart Hickinbottom 2005
2#
3# $Id: LazySearch2.pm 163 2006-09-07 14:07:38Z stuarth $
4#
5# This code is derived from code with the following copyright message:
6#
7# SlimServer Copyright (c) 2001-2005 Sean Adams, Slim Devices Inc.
8# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License,
10# version 2.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16
17# This is a plugin to implement lazy searching using the SqueezeBox remote
18# control. Note that the SlimServer music database must be cleared and rebuilt
19# after installing this plugin.
20#
21# For further details see:
22# http://hickinbottom.demon.co.uk/SlimServer/lazy_searching2.htm
23
24use strict;
25use warnings;
26
27package Plugins::LazySearch2;
28
29use Slim::Utils::Strings qw (string);
30use Slim::Utils::Misc;
31use Slim::Utils::Text;
32use Slim::Utils::Timers;
33use Time::HiRes;
34
35# Name of this plugin - used for various global things to prevent clashes with
36# other plugins.
37use constant PLUGIN_NAME => 'PLUGIN_LAZYSEARCH2';
38
39# Name of the menu item that is expected to be added to the home menu.
40use constant LAZYSEARCH_HOME_MENUITEM => 'LazySearch2';
41
42# Mode for main lazy search mode and lazy search menu.
43use constant LAZYSEARCH_TOP_MODE           => 'PLUGIN_LAZYSEARCH2.topmode';
44use constant LAZYSEARCH_CATEGORY_MENU_MODE => 'PLUGIN_LAZYSEARCH2.categorymenu';
45use constant LAZYBROWSE_MODE               => 'PLUGIN_LAZYSEARCH2.browsemode';
46
47# Preference ranges and defaults.
48use constant LAZYSEARCH_MINLENGTH_MIN            => 2;
49use constant LAZYSEARCH_MINLENGTH_MAX            => 9;
50use constant LAZYSEARCH_MINLENGTH_ARTIST_DEFAULT => 3;
51use constant LAZYSEARCH_MINLENGTH_ALBUM_DEFAULT  => 3;
52use constant LAZYSEARCH_MINLENGTH_GENRE_DEFAULT  => 3;
53use constant LAZYSEARCH_MINLENGTH_TRACK_DEFAULT  => 4;
54use constant LAZYSEARCH_LEFTDELETES_DEFAULT      => 1;
55use constant LAZYSEARCH_HOOKSEARCHBUTTON_DEFAULT => 1;
56
57# Constants that control the background lazy search database encoding.
58use constant LAZYSEARCH_RESCAN_END_CHECK_FREQ => 10;
59use constant LAZYSEARCH_ENCODE_MAX_QUANTA     => 0.5;
60
61# Export the version to the server (as a subversion keyword).
62use vars qw($VERSION);
63$VERSION = 'branch-6_2-r163';
64
65# The DataStore. We keep this as global to avoid keep looking it up.
66my $ds;
67
68# Whether the awful workaround for Bugzilla bug #2511/#2531 is necessary. See
69# the use of this variable later for the gory details. If you don't use
70# playlists or introduce new music through "browse music folders" then you
71# should be able to set this to '0' to reduce the time taken for 'rescan'
72# operations (perhaps preventing player stalls).
73my $needArtistPlaylistBodge = 1;
74
75# This hash-of-hashes contains state information on the current lazy search for
76# each player. The first hash index is the player (eg $clientMode{$client}),
77# and the second a 'parameter' for that player.
78# The elements of the second hash are as follows:
79#   search_items:   This holds the result of the find() operation, and will
80#                   be an array of album, artist or track objects.
81#   search_text:    The current search text (ie the number keys on the remote
82#                   control).
83#   search_performed:   A Boolean flag indicating whether a search has yet
84#                       been performed (and hence whether search_items has
85#                       the search results).
86#   select_col:         This is the 'field' that the find returns.
87#   search_col:         This is the column that the lazy search is applied to
88#                       when performing the find().
89#   sortby_col:         The column used to order the result of the find().
90#   player_title:       Start of line1 player text when there are search
91#                       results.
92#   player_title_empty: The line1 text when no search has yet been performed.
93#   enter_more_prompt:  The line2 prompt shown when there is insufficient
94#                       search text entered to perform the search.
95#   min_search_length:  The minimum number of characters that must be entered
96#                       before the lazy search is performed.
97#   gettext:            Function reference to a method that can return the
98#                       next to display for a find-returned item.
99#   onright:            Function reference to a method that enters a browse
100#                       mode on the item being displayed.
101#   playlevel:          Identifying the type of item to be added to the
102#                       playlist if the user presses PLAY/ADD/INSERT on a
103#                       search result.
104my %clientMode = ();
105
106# Hash of outstanding database objects that are to be lazy-encoded. This is
107# present to allow a background task to work on them in chunks, preventing
108# performance problems caused by the server going busy for a long time.
109# The structure of the hash is as follows:
110#   type => { lazify_sub => XX, ids => (YY) }
111# Where:
112#   type is 'album', 'artist', 'genre' or 'track'.
113#   XX is a subroutine that will update the appropriate 'search' attribute of
114#       the object
115#   YY is a list (array) of IDs to process.
116my %encodeQueues = ();
117
118# Flag to protect against multiple initialisation or shutdown
119my $initialised = 0;
120
121# Flag to indicate whether we're currently applying 'lazification' to the
122# database. Used to detect and warn the user of this when entering
123# lazy search mode while this is in progress.
124my $lazifying_database = 0;
125
126# Map which is used to quickly translate the button pushes captured by our
127# mode back into the numbers on those keys.
128my %numberScrollMap = (
129    'numberScroll_0' => '0',
130    'numberScroll_1' => '1',
131    'numberScroll_2' => '2',
132    'numberScroll_3' => '3',
133    'numberScroll_4' => '4',
134    'numberScroll_5' => '5',
135    'numberScroll_6' => '6',
136    'numberScroll_7' => '7',
137    'numberScroll_8' => '8',
138    'numberScroll_9' => '9',
139);
140
141# Below are functions that are part of the standard SlimServer plugin
142# interface.
143
144# Main mode of this plugin; offers the artist/album/genre/song browse options
145sub setMode {
146    my $client = shift;
147    my $method = shift || '';
148
149    $::d_plugins
150      && Slim::Utils::Misc::msg("LazySearch2: setMode(method=$method)\n");
151
152    # Handle request to exit our mode.
153    if ( $method eq 'pop' ) {
154        leaveMode($client);
155
156        # Pop the current mode off the mode stack and restore the previous one
157        Slim::Buttons::Common::popMode($client);
158        return;
159    }
160
161    # Use INPUT.Choice to display the top-level search menu choices.
162    my %params = (
163
164        # The header (first line) to display whilst in this mode.
165        header => '{LINE1_BROWSE} {count}',
166
167        # A reference to the list of items to display.
168        listRef => [qw({ARTISTS} {ALBUMS} {GENRES} {SONGS})],
169
170        # A unique name for this mode that won't actually get displayed
171        # anywhere.
172        modeName => LAZYSEARCH_CATEGORY_MENU_MODE,
173
174        # An anonymous function that is called every time the user presses the
175        # RIGHT button.
176        onRight => sub {
177            my ( $client, $item ) = @_;
178
179            # Search term initially empty.
180            $clientMode{$client}{search_text}      = '';
181            $clientMode{$client}{search_items}     = ();
182            $clientMode{$client}{search_performed} = 0;
183
184            if ( $item eq '{ARTISTS}' ) {
185                $clientMode{$client}{select_col}   = 'artist';
186                $clientMode{$client}{search_col}   = 'contributor.namesearch';
187                $clientMode{$client}{sortby_col}   = 'artist';
188                $clientMode{$client}{player_title} = '{LINE1_BROWSE_ARTISTS}';
189                $clientMode{$client}{player_title_empty} =
190                  '{LINE1_BROWSE_ARTISTS_EMPTY}';
191                $clientMode{$client}{enter_more_prompt} =
192                  'LINE2_ENTER_MORE_ARTISTS';
193                $clientMode{$client}{min_search_length} =
194                  Slim::Utils::Prefs::get(
195                    'plugin-lazysearch2-minlength-artist');
196                $clientMode{$client}{gettext} = sub { return shift->name; };
197                $clientMode{$client}{onright} = sub {
198                    my ( $client, $item ) = @_;
199
200                    # Browse albums by this artist.
201                    Slim::Buttons::Common::pushModeLeft(
202                        $client,
203                        'browsedb',
204                        {
205                            'hierarchy'    => 'artist,album,track',
206                            'level'        => 1,
207                            'findCriteria' => { 'artist' => $item->{'id'} },
208                        }
209                    );
210                };
211                $clientMode{$client}{playlevel} = 'artist';
212                setSearchBrowseMode( $client, $item, 0 );
213            } elsif ( $item eq '{ALBUMS}' ) {
214                $clientMode{$client}{select_col}   = 'album';
215                $clientMode{$client}{search_col}   = 'album.titlesearch';
216                $clientMode{$client}{sortby_col}   = 'album';
217                $clientMode{$client}{player_title} = '{LINE1_BROWSE_ALBUMS}';
218                $clientMode{$client}{player_title_empty} =
219                  '{LINE1_BROWSE_ALBUMS_EMPTY}';
220                $clientMode{$client}{enter_more_prompt} =
221                  'LINE2_ENTER_MORE_ALBUMS';
222                $clientMode{$client}{min_search_length} =
223                  Slim::Utils::Prefs::get('plugin-lazysearch2-minlength-album');
224                $clientMode{$client}{gettext} = sub { return shift->title; };
225                $clientMode{$client}{onright} = sub {
226                    my ( $client, $item ) = @_;
227
228                    # Browse tracks for this album.
229                    Slim::Buttons::Common::pushModeLeft(
230                        $client,
231                        'browsedb',
232                        {
233                            'hierarchy'    => 'track',
234                            'level'        => 0,
235                            'findCriteria' => { 'album' => $item->{'id'} },
236                        }
237                    );
238                };
239                $clientMode{$client}{playlevel} = 'album';
240                setSearchBrowseMode( $client, $item, 0 );
241            } elsif ( $item eq '{GENRES}' ) {
242                $clientMode{$client}{select_col}   = 'genre';
243                $clientMode{$client}{search_col}   = 'genre.namesearch';
244                $clientMode{$client}{sortby_col}   = 'name';
245                $clientMode{$client}{player_title} = '{LINE1_BROWSE_GENRES}';
246                $clientMode{$client}{player_title_empty} =
247                  '{LINE1_BROWSE_GENRES_EMPTY}';
248                $clientMode{$client}{enter_more_prompt} =
249                  'LINE2_ENTER_MORE_GENRES';
250                $clientMode{$client}{min_search_length} =
251                  Slim::Utils::Prefs::get('plugin-lazysearch2-minlength-genre');
252                $clientMode{$client}{gettext} = sub { return shift->name; };
253                $clientMode{$client}{onright} = sub {
254                    my ( $client, $item ) = @_;
255
256                    # Browse artists by this genre.
257                    Slim::Buttons::Common::pushModeLeft(
258                        $client,
259                        'browsedb',
260                        {
261                            'hierarchy'    => 'artist,album,track',
262                            'level'        => 0,
263                            'findCriteria' => { 'genre' => $item->{'id'} },
264                        }
265                    );
266                };
267                $clientMode{$client}{playlevel} = 'genre';
268                setSearchBrowseMode( $client, $item, 0 );
269            } elsif ( $item eq '{SONGS}' ) {
270                $clientMode{$client}{select_col}   = 'track';
271                $clientMode{$client}{search_col}   = 'track.titlesearch';
272                $clientMode{$client}{sortby_col}   = 'title';
273                $clientMode{$client}{player_title} = '{LINE1_BROWSE_TRACKS}';
274                $clientMode{$client}{player_title_empty} =
275                  '{LINE1_BROWSE_TRACKS_EMPTY}';
276                $clientMode{$client}{enter_more_prompt} =
277                  'LINE2_ENTER_MORE_TRACKS';
278                $clientMode{$client}{min_search_length} =
279                  Slim::Utils::Prefs::get('plugin-lazysearch2-minlength-track');
280                $clientMode{$client}{gettext} = sub { return shift->title; };
281                $clientMode{$client}{onright} = sub {
282                    my ( $client, $item ) = @_;
283
284                    # Push into the trackinfo mode for this one track.
285                    my $track = $ds->objectForId( 'track', $item->{'id'} );
286                    Slim::Buttons::Common::pushModeLeft( $client, 'trackinfo',
287                        { 'track' => $track->url } );
288                };
289                $clientMode{$client}{playlevel} = 'track';
290                setSearchBrowseMode( $client, $item, 0 );
291            }
292
293            # If rescan is in progress then warn the user.
294            if ( $lazifying_database || Slim::Music::Import::stillScanning() ) {
295                $::d_plugins
296                  && Slim::Utils::Misc::msg(
297                    "LazySearch2: entering search while scan in progress\n");
298                if ( $client->linesPerScreen == 1 ) {
299                    $client->showBriefly(
300                        {
301                            'line1' => $client->doubleString('SCAN_IN_PROGRESS')
302                        }
303                    );
304                } else {
305                    $client->showBriefly(
306                        { 'line1' => string('SCAN_IN_PROGRESS') } );
307                }
308            }
309        },
310
311        # These are all menu items and so have a right-arrow overlay
312        overlayRef => sub {
313            return [ undef, Slim::Display::Display::symbol('rightarrow') ];
314        },
315    );
316
317    # Find the datastore we're going to do queries on later. This allows it
318    # to change inbetween invocations of this mode (although I don't know why
319    # it would change), without re-querying what it is excessively.
320    $ds = Slim::Music::Info->getCurrentDataStore();
321
322    # Use our INPUT.Choice-derived mode to show the menu and let it do all the
323    # hard work of displaying the list, moving it up and down, etc, etc.
324    if ( $method eq 'push' ) {
325        Slim::Buttons::Common::pushModeLeft( $client,
326            LAZYSEARCH_CATEGORY_MENU_MODE, \%params );
327    } else {
328        Slim::Buttons::Common::pushMode( $client, LAZYSEARCH_CATEGORY_MENU_MODE,
329            \%params );
330        $client->update();
331    }
332}
333
334# Function called when leaving our top-level lazy search menu mode. We use this
335# to track whether or not the user is within the lazy search mode for a
336# particular player.
337sub leaveMode {
338    my $client = shift;
339
340    $::d_plugins
341      && Slim::Utils::Misc::msg(
342        "LazySearch2: leaving top-level lazy search menu mode\n");
343
344    # Clear the search results to save a little memory.
345    $clientMode{$client}{search_items} = ();
346}
347
348# There are no functions in this mode as the main mode (the top-level menu) is
349# all handled by the INPUT.Choice mode.
350sub getFunctions {
351    return {};
352}
353
354# Return the name of this plugin; this goes on the server setting plugin
355# page, for example.
356sub getDisplayName {
357    return PLUGIN_NAME;
358}
359
360# Set up this plugin when it's inserted or the server started. Adds our hooks
361# for database encoding and makes our customised mode that lets us grab and
362# process extra buttons.
363sub initPlugin() {
364    return if $initialised;    # don't need to do it twice
365
366    $::d_plugins
367      && Slim::Utils::Misc::msg("LazySearch2: Initialising $VERSION\n");
368
369    # Remember we're now initialised. This prevents multiple-initialisation,
370    # which may otherwise cause trouble with duplicate hooks or modes.
371    $initialised = 1;
372
373    # Make sure the preferences are set to something sensible before we call
374    # on them later.
375    checkDefaults();
376
377    # Add a command callback. This is used to detect and act upon database
378    # rescans.
379    Slim::Control::Command::setExecuteCallback(
380        \&Plugins::LazySearch2::commandCallback );
381
382    # Top-level menu mode. We register a custom INPUT.Choice mode so that
383    # we can detect when we're in it (for SEARCH button toggle).
384    Slim::Buttons::Common::addMode( LAZYSEARCH_TOP_MODE, undef, \&setMode );
385    Slim::Buttons::Common::addMode(
386        LAZYSEARCH_CATEGORY_MENU_MODE,
387        Slim::Buttons::Input::Choice::getFunctions(),
388        \&Slim::Buttons::Input::Choice::setMode
389    );
390
391    # Out input map for the new categories menu mode, based on thd default map
392    # contents for INPUT.Choice.
393    my %categoryInputMap = (
394        'arrow_left'  => 'exit_left',
395        'arrow_right' => 'exit_right',
396        'play'        => 'play',
397        'add'         => 'add',
398        'stop'        => 'passback',
399        'pause'       => 'passback',
400    );
401    for my $buttonPressMode (qw{repeat hold hold_release single double}) {
402        $categoryInputMap{ 'play.' . $buttonPressMode }   = 'dead';
403        $categoryInputMap{ 'add.' . $buttonPressMode }    = 'dead';
404        $categoryInputMap{ 'search.' . $buttonPressMode } = 'dead';
405        $categoryInputMap{ 'stop.' . $buttonPressMode }   = 'passback';
406        $categoryInputMap{ 'pause.' . $buttonPressMode }  = 'passback';
407    }
408    Slim::Hardware::IR::addModeDefaultMapping( LAZYSEARCH_CATEGORY_MENU_MODE,
409        \%categoryInputMap );
410
411    # Make a customised version of the INPUT.Choice mode so that we can grab
412    # the numbers (INPUT.Choice will normally use these to scroll through
413    # 'numberScroll').
414    $::d_plugins
415      && Slim::Utils::Misc::msg(
416        "LazySearch2: making custom INPUT.Choice-derived modes\n");
417    my %chFunctions = %{ Slim::Buttons::Input::Choice::getFunctions() };
418    $chFunctions{'numberScroll'} = \&lazyKeyHandler;
419    $chFunctions{'play'}         = \&onPlayHandler;
420    $chFunctions{'addSingle'}    = \&onAddHandler;
421    $chFunctions{'addHold'}      = \&onInsertHandler;
422    $chFunctions{'leftSingle'}   = \&onDelCharHandler;
423    $chFunctions{'leftHold'}     = \&onDelAllHandler;
424    $chFunctions{'forceSearch'}  = \&lazyForceSearch;
425    Slim::Buttons::Common::addMode( LAZYBROWSE_MODE, \%chFunctions,
426        \&Slim::Buttons::Input::Choice::setMode );
427
428    # Out input map for the new lazy browse mode, based on the default map
429    # contents for INPUT.Choice.
430    my %lazyInputMap = (
431        'arrow_left'      => 'leftSingle',
432        'arrow_left.hold' => 'leftHold',
433        'arrow_right'     => 'exit_right',
434        'play'            => 'play',
435        'pause.single'    => 'pause',
436        'pause.hold'      => 'stop',
437        'add.single'      => 'addSingle',
438        'add.hold'        => 'addHold',
439        'search'          => 'forceSearch',
440    );
441    for my $buttonPressMode (qw{repeat hold hold_release single double}) {
442        $lazyInputMap{ 'play.' . $buttonPressMode }   = 'dead';
443        $lazyInputMap{ 'search.' . $buttonPressMode } = 'dead';
444    }
445    Slim::Hardware::IR::addModeDefaultMapping( LAZYBROWSE_MODE,
446        \%lazyInputMap );
447
448    # Intercept the 'search' button to take us to our top-level menu.
449    Slim::Buttons::Common::setFunction( 'search', \&lazyOnSearch );
450
451    # Check the database to ensure that the lazy search index has been
452    # built. This is most useful on first install as it will perform the
453    # initial database build.
454    $::d_plugins
455      && Slim::Utils::Misc::msg(
456        "LazySearch2: scheduling lazification on plugin initialisation\n");
457    startScanEndMonitor();
458}
459
460sub shutdownPlugin() {
461    return if !$initialised;    # don't need to do it twice
462
463    $::d_plugins && Slim::Utils::Misc::msg("LazySearch2: Shutting down\n");
464
465    # Remove the command callback we'd previously registered
466    Slim::Control::Command::clearExecuteCallback(
467        \&Plugins::LazySearch2::commandCallback );
468
469    # @@TODO@@
470    # Do we need to remove our top-level mode?
471
472    # We're no longer initialised.
473    $initialised = 0;
474}
475
476# Return information on this plugin's settings. The web interface will then
477# present those on the 'server settings->plugins' page.
478sub setupGroup {
479    my %setupGroup = (
480        PrefOrder => [
481            'plugin-lazysearch2-minlength-artist',
482            'plugin-lazysearch2-minlength-album',
483            'plugin-lazysearch2-minlength-genre',
484            'plugin-lazysearch2-minlength-track',
485            'plugin-lazysearch2-leftdeletes',
486            'plugin-lazysearch2-hooksearchbutton',
487            'plugin-lazysearch2-lazifynow'
488        ],
489        GroupHead         => string('SETUP_GROUP_PLUGIN_LAZYSEARCH2'),
490        GroupDesc         => string('SETUP_GROUP_PLUGIN_LAZYSEARCH2_DESC'),
491        GroupLine         => 1,
492        GroupSub          => 1,
493        Suppress_PrefSub  => 1,
494        Suppress_PrefLine => 1,
495    );
496
497    my %setupPrefs = (
498        'plugin-lazysearch2-minlength-artist' => {
499            'validate'     => \&Slim::Web::Setup::validateInt,
500            'validateArgs' =>
501              [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
502            'PrefHead' => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST'),
503            'PrefDesc' =>
504              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_DESC'),
505            'PrefChoose' =>
506              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHOOSE'),
507            'changeIntro' =>
508              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHANGE'),
509        },
510        'plugin-lazysearch2-minlength-album' => {
511            'validate'     => \&Slim::Web::Setup::validateInt,
512            'validateArgs' =>
513              [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
514            'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM'),
515            'PrefChoose' =>
516              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHOOSE'),
517            'changeIntro' =>
518              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHANGE'),
519        },
520        'plugin-lazysearch2-minlength-genre' => {
521            'validate'     => \&Slim::Web::Setup::validateInt,
522            'validateArgs' =>
523              [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
524            'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE'),
525            'PrefChoose' =>
526              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHOOSE'),
527            'changeIntro' =>
528              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHANGE'),
529        },
530        'plugin-lazysearch2-minlength-track' => {
531            'validate'     => \&Slim::Web::Setup::validateInt,
532            'validateArgs' =>
533              [ LAZYSEARCH_MINLENGTH_MIN, LAZYSEARCH_MINLENGTH_MAX ],
534            'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK'),
535            'PrefChoose' =>
536              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHOOSE'),
537            'changeIntro' =>
538              string('SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHANGE'),
539        },
540        'plugin-lazysearch2-leftdeletes' => {
541            'validate'   => \&Slim::Web::Setup::validateTrueFalse,
542            'PrefHead'   => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES'),
543            'PrefDesc'   => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_DESC'),
544            'PrefChoose' =>
545              string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHOOSE'),
546            'changeIntro' =>
547              string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHANGE'),
548            'options' => {
549                '1' => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_1'),
550                '0' => string('SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_0')
551            },
552        },
553        'plugin-lazysearch2-hooksearchbutton' => {
554            'validate' => \&Slim::Web::Setup::validateTrueFalse,
555            'PrefHead' => string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON'),
556            'PrefDesc' =>
557              string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_DESC'),
558            'PrefChoose' =>
559              string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHOOSE'),
560            'changeIntro' =>
561              string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHANGE'),
562            'options' => {
563                '1' => string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_1'),
564                '0' => string('SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_0')
565            },
566        },
567        'plugin-lazysearch2-lazifynow' => {
568            'validate'    => \&Slim::Web::Setup::validateAcceptAll,
569            'PrefHead'    => string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW'),
570            'PrefDesc'    => string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_DESC'),
571            'changeIntro' =>
572              string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_CHANGE'),
573            'inputTemplate' => 'setup_input_submit.html',
574            'ChangeButton'  =>
575              string('SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_BUTTON'),
576            'onChange' => sub {
577                if ( !$lazifying_database ) {
578                    $::d_plugins
579                      && Slim::Utils::Misc::msg(
580                        "LazySearch2: manual lazification requested\n");
581                    startScanEndMonitor();
582                }
583            },
584            'dontSet'   => 1,
585            'changeMsg' => '',
586        },
587    );
588
589    return ( \%setupGroup, \%setupPrefs );
590}
591
592# Below are functions that are specific to this plugin.
593
594# Sub-mode, allowing entry search and browsing within a search category. This
595# uses our custom mode to combine standard INPUT.Choice functionality with
596# our handlers that catch the number keys (which would normally scroll the
597# choices), and the play/add/insert buttons that we need so that we can
598# manipulate the playlist as appropriate. This mode is driven through the
599# clientMode hash (see its description at the head of this file).
600sub setSearchBrowseMode {
601    my $client = shift;
602    my $method = shift;
603    my $silent = shift;
604
605    # Handle request to exit our mode.
606    if ( $method eq 'pop' ) {
607        $::d_plugins
608          && Slim::Utils::Misc::msg(
609            "LazySearch2: popping out of searchBrowse mode\n");
610
611        # Pop the current mode off the mode stack and restore the previous one
612        Slim::Buttons::Common::popMode($client);
613        return;
614    }
615
616    # The items for the list are those returned by the search (if there was
617    # one), or the defined 'enter more' prompt if not.
618    my $itemsRef;
619    my $headerString;
620    if ( ( length $clientMode{$client}{search_text} ) > 0 ) {
621        $headerString =
622            $clientMode{$client}{player_title} . ' \''
623          . $clientMode{$client}{search_text} . '\'';
624    } else {
625        $headerString = $clientMode{$client}{player_title_empty};
626    }
627
628    # If we've actually performed a search then the title also includes
629    # the item number/total items as per normal browse modes.
630    if ( $clientMode{$client}{search_performed} ) {
631        $itemsRef = $clientMode{$client}{search_items};
632        $headerString .= ' {count}';
633    } else {
634        @$itemsRef =
635          ( $client->string( $clientMode{$client}{enter_more_prompt} ) );
636    }
637
638    # Parameters for our INPUT.Choice-derived mode.
639    my %params = (
640
641        # Text title on list1.
642        header => $headerString,
643
644        # A reference to the list of items to display.
645        listRef => $itemsRef,
646
647        # The function to extract the title of each item.
648        name => \&lazyGetText,
649
650        # Name for this mode.
651        modeName => LAZYBROWSE_MODE,
652
653        # Catch and handle the RIGHT button.
654        onRight => \&lazyOnRight,
655
656        # A handler that manages play/add/insert (differentiated by the
657        # last parameter).
658        onPlay => sub {
659            my ( $client, $item, $addMode ) = @_;
660
661            # Start playing the item selected (in the correct mode - play, add
662            # or insert).
663            lazyOnPlay( $client, $item, $addMode );
664        },
665
666        # These are all browsable items and so have a right-arrow overlay,
667        # but if the list is empty then there is never an overlay.
668        overlayRef => sub {
669            my ( $client, $item ) = @_;
670            my $listRef = $client->param('listRef');
671            if ( !$clientMode{$client}{search_performed}
672                || ( scalar(@$listRef) == 0 ) )
673            {
674                return [ undef, undef ];
675            } else {
676                return [ undef, Slim::Display::Display::symbol('rightarrow') ];
677            }
678        },
679    );
680
681    # Use the new mode defined by INPUT.Choice and let it do all the hard work
682    # of displaying the list, moving it up and down, etc, etc. We have a silent
683    # version that doesn't scroll the mode in, which is used for subsequent
684    # narrowing of the search (which will be replacing an already-displayed
685    # version of this mode).
686    if ($silent) {
687        Slim::Buttons::Common::pushMode( $client, LAZYBROWSE_MODE, \%params );
688    } else {
689        Slim::Buttons::Common::pushModeLeft( $client, LAZYBROWSE_MODE,
690            \%params );
691    }
692}
693
694# Subroutine to extract the text to show for the browse/search. Most of this
695# is stock here, we just need to defer to a specific function stored in the
696# clientMode hash to get the actual text, as that differs for each item class.
697sub lazyGetText {
698    my ( $client, $item ) = @_;
699
700    if ( !$clientMode{$client}{search_performed} ) {
701        return $client->string( $clientMode{$client}{enter_more_prompt} );
702    } else {
703        my $listRef = $client->param('listRef');
704        if ( scalar(@$listRef) == 0 ) {
705            return $client->string('EMPTY');
706        } else {
707            my $getTextFunction = $clientMode{$client}{gettext};
708            return &$getTextFunction($item);
709        }
710    }
711}
712
713# Make the SEARCH button force a search in the lazy search entry, consistent
714# with the behaviour of the standard SEARCH button.
715sub lazyForceSearch {
716    my $client = shift;
717
718    if ( !$clientMode{$client}{search_performed}
719        && ( length $clientMode{$client}{search_text} > 1 ) )
720    {
721        $::d_plugins
722          && Slim::Utils::Misc::msg("LazySearch2: forcing short text search\n");
723        cancelPendingSearch($client);
724        onFindTimer( 'dummy', $client, 1 );
725    }
726}
727
728# Called when the user presses SEARCH. This allows toggling between the
729# lazy search menu and the standard player search menu. A preference allows
730# the SEARCH button to decide whether it's going to enter the standard or
731# lazy search modes when it's currently in neither.
732sub lazyOnSearch {
733    my $client       = shift;
734    my $mode         = Slim::Buttons::Common::mode($client);
735    my $inLazySearch = ( $mode eq LAZYSEARCH_TOP_MODE )
736      || ( $mode eq LAZYSEARCH_CATEGORY_MENU_MODE )
737      || ( $mode eq LAZYBROWSE_MODE )
738      || 0;
739    my $inSearch = 0;    #@@TODO@@@
740    my $gotoLazy;
741    if ( Slim::Utils::Prefs::get('plugin-lazysearch2-hooksearchbutton') ) {
742
743        # Normal operation - enter lazy search as long as we're not already
744        # in it, in which case we go to original search (allows double-search
745        # to get back to the old mode).
746        $gotoLazy = !$inLazySearch || 0;
747    } elsif ($inSearch) {
748
749        # If in original search mode we always enter lazy search.
750        $gotoLazy = 1;
751    } else {
752
753        # Go into the standard search.
754        $gotoLazy = 0;
755    }
756
757    $::d_plugins
758      && Slim::Utils::Misc::msg(
759"LazySearch2: SEARCH button (mode='$mode', inSearch=$inSearch, inLazySearch=$inLazySearch, gotoLazy=$gotoLazy)\n"
760      );
761
762    if ($gotoLazy) {
763
764        # @@TODO@@
765        # This doesn't seem to work properly - it doesn't seem to set the top
766        # menu to the lazy item prior to jumping in. It seems to work,
767        # but when existing the mode it doesn't exit to the lazy top
768        # level item
769        Slim::Buttons::Common::setMode( $client, 'home' );
770        Slim::Buttons::Home::jump( $client, LAZYSEARCH_HOME_MENUITEM );
771        Slim::Buttons::Common::pushMode( $client, LAZYSEARCH_TOP_MODE );
772    } else {
773
774        # Into the normal search menu.
775        Slim::Buttons::Home::jumpToMenu( $client, "SEARCH" );
776    }
777}
778
779# Subroutine to perform the 'browse into' RIGHT button handler for lazy search
780# results. The browse mode just differs by the method used to start browsing
781# for each type, and that's stored in the clientMode hash.
782sub lazyOnRight {
783    my ( $client, $item ) = @_;
784
785    # If the list is empty then don't push into browse mode
786    my $listRef = $client->param('listRef');
787    if ( scalar(@$listRef) == 0 ) {
788        $client->bumpRight();
789    } else {
790
791        # Only allow right if we've performed a search.
792        if ( $clientMode{$client}{search_performed} ) {
793
794            # Cancel any pending timer.
795            cancelPendingSearch($client);
796
797            # Push into the item details, using database browse.
798            # The method executed is stored in the hash.
799            my $onRightFunction = $clientMode{$client}{onright};
800            return &$onRightFunction( $client, $item );
801        }
802    }
803}
804
805# Handle press of play/add/insert when on an item returned from the search.
806# addMode=1 : add
807# addMode=2 : insert
808# addMode=3 : play
809sub lazyOnPlay {
810    my ( $client, $item, $addMode ) = @_;
811
812    # Cancel any pending timer.
813    cancelPendingSearch($client);
814
815    # If no list loaded (eg search returned nothing), or
816    # user has not entered enough text yet, then ignore the
817    # command.
818    my $listRef = $client->param('listRef');
819    if ( !$clientMode{$client}{search_performed} ) {
820        return;
821    }
822
823    my $id = $item->{'id'};
824    $::d_plugins
825      && Slim::Utils::Misc::msg(
826        "LazySearch2: play on track search (id $id), addMode=$addMode\n");
827    my ( $line1, $line2, $msg, $cmd );
828
829    if ( $addMode == 1 ) {
830        $msg = "ADDING_TO_PLAYLIST";
831        $cmd = "addtracks";
832    } elsif ( $addMode == 2 ) {
833        $msg = "INSERT_TO_PLAYLIST";
834        $cmd = "inserttracks";
835    } else {
836        $msg =
837          Slim::Player::Playlist::shuffle($client)
838          ? "PLAYING_RANDOMLY_FROM"
839          : "NOW_PLAYING_FROM";
840        $cmd = "loadtracks";
841    }
842
843    if ( $client->linesPerScreen == 1 ) {
844        $line1 = $client->doubleString($msg);
845    } else {
846        $line1 = $client->string($msg);
847        my $getTextFunction = $clientMode{$client}{gettext};
848        $line2 = $item->{'name'};
849    }
850    $::d_plugins
851      && Slim::Utils::Misc::msg("LazySearch2: $msg ($line1, $line2)\n");
852    $client->showBriefly(
853        {
854            'line1' => $line1,
855            'line2' => $line2
856        }
857    );
858    $client->execute(
859        [
860            'playlist', $cmd,
861            sprintf( $clientMode{$client}{playlevel} . '=%d', $id )
862        ]
863    );
864
865    # Not sure why, but we don't need to start the play
866    # here - seems something by default is grabbing and
867    # processing the button. Strange...
868}
869
870# Pick up each number button press and add it to the current lazy search text,
871# then re-search using that text.
872sub lazyKeyHandler {
873    my ( $client, $method ) = @_;
874
875    my $listIndex = $client->param('listIndex');
876    my $items     = $client->param('listRef');
877    my $item      = $items->[$listIndex];
878
879    # Map the scroll number (the method invoked by the INPUT.Choice button
880    # the lazy browse mode is based on), to a real number character.
881    my $numberKey = $numberScrollMap{$method};
882    $::d_plugins
883      && Slim::Utils::Misc::msg(
884        "LazySearch2: lazy key entry, method=$method, number=$numberKey\n");
885    $clientMode{$client}{search_text} .= $numberKey;
886
887    # Update the display.
888    updateLazyEntry( $client, $item );
889
890    # Cancel any pending search and schedule another, so search happens
891    # n seconds after the last button press.
892    addPendingSearch($client);
893}
894
895# Update the display during lazy search entry. This is used on change of the
896# lazy search text (ie add character or delete character).
897sub updateLazyEntry {
898    my ( $client, $item ) = @_;
899
900    # Pop back into the mode to get the display updated. We ask for a 'silent'
901    # update, which prevents the mode scrolling back in again.
902    Slim::Buttons::Common::popMode($client);
903    setSearchBrowseMode( $client, $item, 1 );
904    $client->update();
905
906    # In one-line display modes show a clue to the user, as he won't see the
907    # result of the search for some time and won't otherwise get any visual
908    # feedback.
909    if ( $client->linesPerScreen == 1 ) {
910        my $line =
911            $client->string('SHOWBRIEFLY_DISPLAY') . ' \''
912          . $clientMode{$client}{search_text} . '\'';
913        $client->showBriefly( { 'line1' => $line } );
914    }
915}
916
917# Schedule a new search to occur for the specified client.
918sub addPendingSearch($) {
919    my $client = shift;
920
921    # Schedule a timer. Any existing one is cancelled first as we only allow
922    # one outstanding one for this player.
923    cancelPendingSearch($client);
924
925    my $timerName = PLUGIN_NAME . Slim::Player::Client::id($client);
926    $::d_plugins
927      && Slim::Utils::Misc::msg(
928        "LazySearch2: adding timer called $timerName\n");
929    Slim::Utils::Timers::setTimer( $timerName,
930        Time::HiRes::time() + Slim::Utils::Prefs::get("displaytexttimeout"),
931        \&onFindTimer, $client );
932}
933
934# Remove any outstanding lazy search timer. This is used when either leaving
935# the search mode altogether, or when another key has been entered by the user
936# (as a new later search will be scheduled instead).
937sub cancelPendingSearch($) {
938    my $client = shift;
939
940    my $timerName = PLUGIN_NAME . Slim::Player::Client::id($client);
941    $::d_plugins
942      && Slim::Utils::Misc::msg(
943        "LazySearch2: cancelling timer called $timerName\n");
944
945    # This seems tolerant of timers that don't exist, so no need to make sure
946    # we actually have one scheduled.
947    Slim::Utils::Timers::killOneTimer( $timerName, \&onFindTimer );
948}
949
950# Actually perform the lazy search and go back into the lazy search mode to
951# get the results displayed.
952sub onFindTimer() {
953    my $timerName   = shift;
954    my $client      = shift;
955    my $forceSearch = shift || 0;
956
957    $::d_plugins
958      && Slim::Utils::Misc::msg(
959"LazySearch2: timer callback for $timerName (forceSearch=$forceSearch)\n"
960      );
961
962    my $listIndex = $client->param('listIndex');
963    my $items     = $client->param('listRef');
964    my $item      = $items->[$listIndex];
965
966    # Perform lazy search, if a minimum length of search text is provided.
967    my $itemsRef = $clientMode{$client}{search_items};
968    if ( ( length $clientMode{$client}{search_text} ) >=
969        $clientMode{$client}{min_search_length} || $forceSearch )
970    {
971        $client->showBriefly(
972            {
973                'line1' => sprintf(
974                    $client->string('LINE1_SEARCHING'),
975                    $clientMode{$client}{search_text}
976                )
977            }
978        );
979        my $findResults = $ds->find(
980            {
981                'field' => $clientMode{$client}{select_col},
982                'find'  => {
983                    $clientMode{$client}{search_col} =>
984                      buildFind( $clientMode{$client}{search_text} )
985                },
986                'sortBy' => $clientMode{$client}{sortby_col},
987            }
988        );
989
990        # Each element of the listRef will be a hash with keys name and id.
991        # This is true for artists, albums and tracks.
992        my @searchItems = ();
993        for my $findItem (@$findResults) {
994            my $text = $clientMode{$client}{gettext}($findItem);
995            my $id   = $findItem->id;
996            push @searchItems, { name => $text, id => $id };
997        }
998        $clientMode{$client}{search_items}     = \@searchItems;
999        $clientMode{$client}{search_performed} = 1;
1000
1001        # Re-enter the search mode to get the display updated.
1002        Slim::Buttons::Common::popMode($client);
1003        setSearchBrowseMode( $client, $item, 1 );
1004        $client->update();
1005    } else {
1006        $clientMode{$client}{search_performed} = 0;
1007    }
1008}
1009
1010# Construct the search terms. This takes into account the 'search substring'
1011# preference to build an appropriate array.
1012sub buildFind($) {
1013    my $searchText      = shift;
1014    my $searchSubstring = ( Slim::Utils::Prefs::get('searchSubString') );
1015    my $searchReturn;
1016
1017    if ($searchSubstring) {
1018        $searchReturn = [ '%' . $searchText . '%' ];
1019    } else {
1020
1021        # Search for start of words only. First word is found by tying it
1022        # to appearing immediately after the lazy version separator ('||'),
1023        # non-first words are found by making sure it appears after a space.
1024        $searchReturn = [
1025            '%' . lazyEncode(' ') . $searchText . '%',
1026            '%||' . $searchText . '%'
1027        ];
1028    }
1029
1030    return $searchReturn;
1031}
1032
1033# Call the play/insert/add handler (passing the parameter to differentiate
1034# which function is actually needed).
1035sub onPlayHandler {
1036    my ( $client, $method ) = @_;
1037    my $onAdd = $client->param('onPlay');
1038
1039    my $listIndex = $client->param('listIndex');
1040    my $items     = $client->param('listRef');
1041    my $item      = $items->[$listIndex];
1042
1043    &$onAdd( $client, $item, 0 );
1044}
1045
1046# Call the play/insert/add handler (passing the parameter to differentiate
1047# which function is actually needed).
1048sub onAddHandler {
1049    my ( $client, $method ) = @_;
1050    my $onAdd = $client->param('onPlay');
1051
1052    my $listIndex = $client->param('listIndex');
1053    my $items     = $client->param('listRef');
1054    my $item      = $items->[$listIndex];
1055
1056    &$onAdd( $client, $item, 1 );
1057}
1058
1059# Call the play/insert/add handler (passing the parameter to differentiate
1060# which function is actually needed).
1061sub onInsertHandler {
1062    my ( $client, $method ) = @_;
1063    my $onAdd = $client->param('onPlay');
1064
1065    my $listIndex = $client->param('listIndex');
1066    my $items     = $client->param('listRef');
1067    my $item      = $items->[$listIndex];
1068
1069    &$onAdd( $client, $item, 2 );
1070}
1071
1072# Remove a single character from the search text. If this drops below the
1073# minimum the user is given the same prompts that he gets when he's entered
1074# less than the minimum search characters.
1075sub onDelCharHandler {
1076    my ( $client, $method ) = @_;
1077
1078    $::d_plugins && Slim::Utils::Misc::msg("LazySearch2: del char called\n");
1079
1080    my $listIndex = $client->param('listIndex');
1081    my $items     = $client->param('listRef');
1082    my $item      = $items->[$listIndex];
1083
1084    my $currentText = $clientMode{$client}{search_text};
1085    if ( ( length($currentText) > 0 )
1086        && Slim::Utils::Prefs::get('plugin-lazysearch2-leftdeletes') )
1087    {
1088
1089        # Remove the right-most character from the string.
1090        $clientMode{$client}{search_text} = substr( $currentText, 0, -1 );
1091
1092        # 'cancel' a previous search if the length of the search text now
1093        # falls below the minimum.
1094        if ( ( length $clientMode{$client}{search_text} ) <
1095            $clientMode{$client}{min_search_length} )
1096        {
1097            $clientMode{$client}{search_performed} = 0;
1098        }
1099
1100        # Update the display.
1101        updateLazyEntry( $client, $item );
1102
1103        # Cancel any pending search and schedule another, so search happens
1104        # n seconds after the last button press.
1105        addPendingSearch($client);
1106    } else {
1107
1108        # Clear the search results to save a little memory.
1109        $clientMode{$client}{search_items} = ();
1110
1111        # Prevent any pending search timer from performing a search once
1112        # we've left this mode.
1113        cancelPendingSearch($client);
1114
1115        # Search string is empty, so pop out.
1116        Slim::Buttons::Common::popModeRight($client);
1117    }
1118}
1119
1120# Clear the current search and reset to the state you get into when no search
1121# text has yet been entered.
1122sub onDelAllHandler {
1123    my ( $client, $method ) = @_;
1124
1125    $::d_plugins && Slim::Utils::Misc::msg("LazySearch2: del all called\n");
1126
1127    my $listIndex = $client->param('listIndex');
1128    my $items     = $client->param('listRef');
1129    my $item      = $items->[$listIndex];
1130
1131    my $currentText = $clientMode{$client}{search_text};
1132    if ( length($currentText) > 0 ) {
1133
1134        # Reset the current search text.
1135        $clientMode{$client}{search_text} = '';
1136
1137        # 'Cancel' any search that may have already been performed.
1138        $clientMode{$client}{search_performed} = 0;
1139
1140        # Update the display.
1141        updateLazyEntry( $client, $item );
1142    }
1143}
1144
1145# Called during initialisation, this makes sure that the plugin preferences
1146# stored are sensible. This has the effect of adding them the first time this
1147# plugin is activated and removing the need to check they're defined in each
1148# case of reading them.
1149sub checkDefaults {
1150    if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-artist') )
1151    {
1152        Slim::Utils::Prefs::set(
1153            'plugin-lazysearch2-minlength-artist',
1154            LAZYSEARCH_MINLENGTH_ARTIST_DEFAULT
1155        );
1156    }
1157    if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-album') )
1158    {
1159        Slim::Utils::Prefs::set(
1160            'plugin-lazysearch2-minlength-album',
1161            LAZYSEARCH_MINLENGTH_ALBUM_DEFAULT
1162        );
1163    }
1164    if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-genre') )
1165    {
1166        Slim::Utils::Prefs::set(
1167            'plugin-lazysearch2-minlength-genre',
1168            LAZYSEARCH_MINLENGTH_GENRE_DEFAULT
1169        );
1170    }
1171    if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-minlength-track') )
1172    {
1173        Slim::Utils::Prefs::set(
1174            'plugin-lazysearch2-minlength-track',
1175            LAZYSEARCH_MINLENGTH_TRACK_DEFAULT
1176        );
1177    }
1178    if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-leftdeletes') ) {
1179        Slim::Utils::Prefs::set( 'plugin-lazysearch2-leftdeletes',
1180            LAZYSEARCH_LEFTDELETES_DEFAULT );
1181    }
1182    if ( !Slim::Utils::Prefs::isDefined('plugin-lazysearch2-hooksearchbutton') )
1183    {
1184        Slim::Utils::Prefs::set(
1185            'plugin-lazysearch2-hooksearchbutton',
1186            LAZYSEARCH_HOOKSEARCHBUTTON_DEFAULT
1187        );
1188    }
1189}
1190
1191# The SlimServer core calls back to this on any command. This function looks
1192# for database rescans (whether 'wipe' or just 'refresh' type), and if it
1193# sees such a command it initiates a background timer that periodically checks
1194# whether the scan is still underway.
1195sub commandCallback {
1196
1197    # These are the two passed parameters
1198    my $client    = shift;
1199    my $paramsRef = shift;
1200
1201    my $slimCommand = @$paramsRef[0];
1202    my $paramOne    = @$paramsRef[1];
1203
1204    # Detect the start of a rescan. We then begin monitoring to detect when
1205    # it's finished.
1206    if ( ( $slimCommand eq 'rescan' ) || ( $slimCommand eq 'wipecache' ) ) {
1207
1208        # A rescan has started. Begin monitoring for when that scan has ended
1209        # so that we can go through and add our lazy-search versions to the
1210        # database.
1211        $::d_plugins
1212          && Slim::Utils::Misc::msg(
1213"LazySearch2: rescan detected; starting to monitor for scan completion\n"
1214          );
1215
1216        startScanEndMonitor();
1217    }
1218
1219    # Bugzilla #2511/#2531.
1220    # DANGER WILL ROBINSON! This is a horrendous bodge, but because SlimServer
1221    # will think that any artist with a changed NAMESEARCH column is a new
1222    # artist during a rescan of playlists (and hence add it back, creating
1223    # apparent duplicates), we cheat here by spotting when that problem is
1224    # about to occur and reset the artists back to their 'non-lazy' forms.
1225    #
1226    # Like I say, this is ugly and may even create playback stalls if there
1227    # are a lot of artists (we can't break this up into task chunks because
1228    # we need to try to make sure it's complete before letting SlimServer start
1229    # the rescan).
1230    #
1231    # IT MAY NOT EVEN WORK, as command callbacks are called after the command
1232    # is executed. This may be OK in this case, though, as the scan is a long
1233    # task and we may be able to get in while the main part of the rescan is
1234    # being performed (ie the music), which doesn't cause the problem, before
1235    # the playlist part of the scan. If the user chooses to just rescan the
1236    # playlists, though, then we might not be so lucky, depending on how
1237    # the scanner works (if it adds a background task then I think that might
1238    # not get executed until after command callback is called).
1239    #
1240    # This ugly bodge (did I mention it's a bodge) can be removed if either:
1241    #   a.  SlimServer starts looking for existing artists based on NAME rather
1242    #       than NAMESEARCH in Contributor.pm.
1243    #   b.  We get our own column in the database to store the lazy search
1244    #       version in.
1245    #   c.  Everyone stops using playlists (like me!).
1246    #
1247    # The 'end of scan' periodic check scheduled above will eventually put the
1248    # lazified versions of the artists back.
1249    #
1250    # This CERTAINLY WON'T FIX the problem of duplicate artists being created
1251    # by browsing into a folder with the new music in it (but you should be OK
1252    # if you bring that music in by a rescan instead).
1253    #
1254    # I don't like this; I feel dirty; but this is /slightly/ better than
1255    # nothing.
1256    if ( ( $slimCommand eq "rescan" ) && $needArtistPlaylistBodge ) {
1257        unlazifyArtists();
1258    }
1259}
1260
1261# This starts a timer to monitor for the end of the core SlimServer database
1262# scan.
1263sub startScanEndMonitor() {
1264    my $timerName = PLUGIN_NAME . '.rescanCheck';
1265
1266    # Remove any previous timer and add our new one.
1267    Slim::Utils::Timers::killTimers( $timerName, \&onRescanTimer );
1268    Slim::Utils::Timers::setTimer( $timerName,
1269        Time::HiRes::time() + LAZYSEARCH_RESCAN_END_CHECK_FREQ,
1270        \&onRescanTimer );
1271}
1272
1273# This function is called periodically whilst a database scan is being
1274# performed (as detected through the command callback above). This checks
1275# whether the scan is still taking place, and if so schedules another timer
1276# in a while. If, however, the scan is found to have finished then that's the
1277# time to initiate the task of encoding each artist, track, genre and album
1278# into a lazy-searchable version.
1279sub onRescanTimer() {
1280    my $timerName = shift;
1281    my $client    = shift;
1282
1283    # Check whether the scan is still going on; if so just schedule another
1284    # timer.
1285    if ( Slim::Music::Import::stillScanning() ) {
1286        my $newTimerName = PLUGIN_NAME . '.rescanCheck';
1287        $::d_plugins
1288          && Slim::Utils::Misc::msg(
1289"LazySearch2: scan still in progress; deferring database lazification\n"
1290          );
1291        Slim::Utils::Timers::setTimer( $newTimerName,
1292            Time::HiRes::time() + LAZYSEARCH_RESCAN_END_CHECK_FREQ,
1293            \&onRescanTimer );
1294    } else {
1295        $::d_plugins
1296          && Slim::Utils::Misc::msg(
1297            "LazySearch2: scan finished; starting database lazification\n");
1298        lazifyDatabase();
1299    }
1300}
1301
1302# This function is called when the music database scan has finished. It
1303# identifies each artist, track and album that has not yet been encoded into
1304# lazy form and schedules a SlimServer background task to encode them.
1305sub lazifyDatabase {
1306
1307    # Convert the albums table.
1308    lazifyDatabaseType(
1309        'album',
1310        'album.titlesearch',
1311        sub {
1312            my $obj = shift;
1313            $obj->titlesearch( lazifyColumn( $obj->titlesearch ) );
1314        }
1315    );
1316
1317    # Convert the artists (contributors) table.
1318    lazifyDatabaseType(
1319        'artist',
1320        'contributor.namesearch',
1321        sub {
1322            my $obj = shift;
1323            $obj->namesearch( lazifyColumn( $obj->namesearch ) );
1324        }
1325    );
1326
1327    # Convert the genres table.
1328    lazifyDatabaseType(
1329        'genre',
1330        'genre.namesearch',
1331        sub {
1332            my $obj = shift;
1333            $obj->namesearch( lazifyColumn( $obj->namesearch ) );
1334        }
1335    );
1336
1337    # Convert the songs (tracks) table.
1338    lazifyDatabaseType(
1339        'track',
1340        'track.titlesearch',
1341        sub {
1342            my $obj = shift;
1343            $obj->titlesearch( lazifyColumn( $obj->titlesearch ) );
1344        }
1345    );
1346
1347    # If there are any items to encode then initialise a background task that
1348    # will do that work in chunks.
1349    if ( scalar keys %encodeQueues ) {
1350        Slim::Utils::Scheduler::add_task( \&encodeTask );
1351        $lazifying_database = 1;
1352    } else {
1353        $::d_plugins
1354          && Slim::Utils::Misc::msg(
1355"LazySearch2: no object types require lazification - no task scheduled\n"
1356          );
1357    }
1358}
1359
1360# Return a lazy-encoded search column. This is a combination of the original
1361# search text (so that the original search function still works), combined with
1362# the lazy-encoded version. A separator is used between the two.
1363sub lazifyColumn {
1364    my $in = shift;
1365    my $out;
1366
1367    # Some 'special' database entries don't have a search text version (I've
1368    # seen that with Various Artists). Leave those alone.
1369    if ( defined($in) && ( $in ne '' ) ) {
1370        $out = $in . '||' . lazyEncode($in);
1371    }
1372
1373    return $out;
1374}
1375
1376# This function examines the database for a specific 'object' type (artist,
1377# album or track), and looks for those which have not yet been lazy-encoded.
1378# Those it finds are added to a global hash that is later worked through from
1379# the background task.
1380sub lazifyDatabaseType {
1381    my $type       = shift;
1382    my $search_col = shift;
1383    my $lazify_sub = shift;
1384    $ds = Slim::Music::Info->getCurrentDataStore();
1385
1386    # Find all items of the required type that have not yet been lazified.
1387    my %todoSet = map { $_ => 1 } $ds->find(
1388        {
1389            'field'  => $type,
1390            'idOnly' => 1,
1391            'find'   => { $search_col => { 'NOT LIKE' => '%||%' } }
1392        }
1393    );
1394
1395    $::d_plugins
1396      && Slim::Utils::Misc::msg(
1397            "LazySearch2: lazify type=$type, search_col=$search_col todo="
1398          . scalar( keys %todoSet )
1399          . "\n" );
1400
1401    # Put these IDs into the queue to be processed. Later, we'll work on these
1402    # in chunks from within a task.
1403    if ( scalar keys %todoSet ) {
1404        my %typeHash = ( lazify_sub => $lazify_sub, ids => [ keys %todoSet ] );
1405        $encodeQueues{$type} = \%typeHash;
1406    }
1407}
1408
1409# Remove the lazification from artists. This is specifically present to work
1410# around the bug that causes artists to be duplicated within the database when
1411# there are playlists present or the user navigates into a music folder.
1412sub unlazifyArtists {
1413    $::d_plugins
1414      && Slim::Utils::Misc::msg(
1415"LazySearch2: removing lazification of artists (Bugzilla #2511/#2531 partial workaround)\n"
1416      );
1417
1418    $ds = Slim::Music::Info->getCurrentDataStore();
1419
1420    # Find all artists.
1421    my @allArtists = $ds->find( { 'field' => 'artist' } );
1422    for my $artist (@allArtists) {
1423        my $nameSearch = $artist->namesearch;
1424        if ( defined($nameSearch) ) {
1425            $nameSearch =~ s/\|\|.*$//;
1426            $artist->namesearch($nameSearch);
1427            $artist->update;
1428        }
1429    }
1430
1431    $::d_plugins
1432      && Slim::Utils::Misc::msg("LazySearch2: lazification removed\n");
1433}
1434
1435# This task function is periodically called by SlimServer when it is 'idle'.
1436# It works through the IDs of the objects that require encoding. They are
1437# encoded in chunks taking a maximum amount of time to keep the server and
1438# players responsive. This function returns 0 when the task has finished, and
1439# 1 when there is more work to do and this function should be called again.
1440sub encodeTask {
1441
1442    # Get a single type hash from the encode queue. It doesn't matter on the
1443    # order they come out of the hash.
1444    my $type        = ( keys %encodeQueues )[0];
1445    my $typeHashRef = $encodeQueues{$type};
1446    my %typeHash    = %$typeHashRef;
1447
1448    # Go through and encode each of the identified IDs. To maintain performance
1449    # we will bail out if this takes more than a half a second.
1450
1451    my $lazifySub  = $typeHash{lazify_sub};
1452    my $ids        = $typeHash{ids};
1453    my $start_time = Time::HiRes::time();
1454    $ds = Slim::Music::Info->getCurrentDataStore();
1455
1456    $::d_plugins
1457      && Slim::Utils::Misc::msg( "LazySearch2: encodeTask called - "
1458          . scalar @$ids
1459          . " $type object(s) remaining\n" );
1460
1461    while (
1462        ( scalar @$ids )
1463        && ( ( Time::HiRes::time() - $start_time ) <
1464            LAZYSEARCH_ENCODE_MAX_QUANTA )
1465      )
1466    {
1467        my $id = pop @$ids;
1468
1469        # Update the search text for this one object and write it back to the
1470        # database. We check that the object was successfully retrived as it
1471        # may have been deleted in the meantime.
1472        my $obj = $ds->objectForId( $type, $id );
1473        if ( defined $obj ) {
1474            &$lazifySub($obj);
1475            $obj->update;
1476        }
1477    }
1478
1479    # If we've exhausted the ids for this type then remove this type from the
1480    # hash. If there are any left, however, we'll leave those in for the task
1481    # next time.
1482    if ( !( scalar @$ids ) ) {
1483        delete $encodeQueues{$type};
1484        $::d_plugins
1485          && Slim::Utils::Misc::msg("LazySearch2: exhaused IDs for $type\n");
1486    }
1487
1488    # Find if there there is more work to do, and if so request that this task
1489    # is rescheduled.
1490    my $reschedule_task;
1491    if ( scalar keys %encodeQueues ) {
1492        $reschedule_task = 1;
1493    } else {
1494        $reschedule_task = 0;
1495
1496        # Clear the global flag indicating the task is in progress.
1497        $lazifying_database = 0;
1498    }
1499
1500    return $reschedule_task;
1501}
1502
1503# Convert a search string to a lazy-entry encoded search string. This includes
1504# both the original search term and a lazy-encoded version. Later, when
1505# searching, both are tried. The original search text is kept in upper-case
1506# and the lazy version is encoded as digits - the latter is because both the
1507# original and lazy encoded version is searched in case the user bothers to
1508# put the search string in properly and this minimises the chance of erroneous
1509# matching.
1510#
1511# called:
1512#   undef
1513sub lazyEncode($) {
1514    my $in_string = shift;
1515    my $out_string;
1516
1517    # This translates each searchable character into the number of the key that
1518    # shares that letter on the remote. Thus, this tells us what keys the user
1519    # will enter if he doesn't bother to multi-tap to get at the later
1520    # characters. Note that space maps to zero.
1521    # We do all this on an upper case version, since upper case is all the user
1522    # can enter through the remote control.
1523    $out_string = uc $in_string;
1524    $out_string =~
1525tr/ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 /2223334445556667777888999912345678900/;
1526
1527    return $out_string;
1528}
1529
1530# Standard plugin function to return our message catalogue. Many thanks to the
1531# following for the translations:
1532#   DE  Dieter (dieterp@patente.de)
1533#   ES  NC)stor (nspedalieri@gmail.com)
1534sub strings {
1535    return '
1536PLUGIN_LAZYSEARCH2
1537    DE  Faulpelz-Suche
1538    EN  Lazy Search Music
1539    ES  BC:squeda Laxa de MC:sica
1540    DA  Lazy Search Music
1541    NL  Lazy Search Music
1542   
1543PLUGIN_LAZYSEARCH2_TOPMENU
1544    DE  Faulpelz-Suche
1545    EN  Lazy Search Music
1546    ES  BC:squeda Laxa de MC:sica
1547    DA  Lazy Search Music
1548    NL  Lazy Search Music
1549   
1550LINE1_BROWSE
1551    DE  Faulpelz-Suche
1552    EN  Lazy Search
1553    ES  BC:squeda Laxa
1554    DA  Lazy Search
1555    NL  Lazy Search
1556   
1557LINE1_SEARCHING
1558    DE  Suchen nach \'%s\' ...
1559    EN  Searching for \'%s\' ...
1560    ES  Buscando \'%s\' ...
1561    DA  SC8ger efter \'%s\' ...
1562    NL  Zoekend naar \'%s\' ...
1563
1564SHOWBRIEFLY_DISPLAY
1565    DE  Faulpelz-Suche
1566    EN  Lazy Search
1567    ES  BC:squeda Laxa
1568    DA  Lazy Search
1569    NL  Lazy Search
1570
1571LINE1_BROWSE_ARTISTS
1572    DE  Passende Interpreten
1573    EN  Artists Matching
1574    ES  Artistas Coincidentes
1575    DA  Kunstner sC8gning
1576    NL  Gevonden Artiesten
1577
1578LINE1_BROWSE_ARTISTS_EMPTY
1579    DE  Faulpelz-Suche nach Interpreten
1580    EN  Lazy Search for Artists
1581    ES  BC:squeda Laxa de Artistas
1582    DA  Lazy Search efter kunstner
1583    NL  Lazy Search naar Artiesten
1584
1585LINE1_BROWSE_ALBUMS
1586    DE  Passende Alben
1587    EN  Albums Matching
1588    ES  Clbumes Coincidentes
1589    DA  Matchende albums
1590    NL  Gevonden Albums
1591
1592LINE1_BROWSE_ALBUMS_EMPTY
1593    DE  Faulpelz-Suche nach Alben
1594    EN  Lazy Search for Albums
1595    ES  BC:squeda Laxa de Clbumes
1596    DA  Lazy Search efter Album
1597    NL  Lazy Search naar Albums
1598   
1599LINE1_BROWSE_TRACKS
1600    DE  Passende Titel
1601    EN  Songs Matching
1602    ES  Canciones Coincidentes
1603    DA  Matchende sange
1604    NL  Gevonden Liedjes
1605
1606LINE1_BROWSE_TRACKS_EMPTY
1607    DE  Faulpelz-Suche nach Titel
1608    EN  Lazy Search for Songs
1609    ES  BC:squeda Laxa de Canciones
1610    DA  Lazy Search efter sange
1611    NL  Lazy Search naar Liedjes
1612
1613LINE1_BROWSE_GENRES
1614    DE  Passende Stilrichtungen
1615    EN  Genres Matching
1616    ES  GC)neros Coincidentes
1617    DA  Matchende genre
1618    NL  Gevonden Genres
1619
1620LINE1_BROWSE_GENRES_EMPTY
1621    DE  Faulpelz-Suche nach Stilrichtungen
1622    EN  Lazy Search for Genres
1623    ES  BC:squeda Laxa de GC)neros
1624    DA  Lazy Search efter genre
1625    NL  Lazy Search naar Genres
1626
1627LINE2_ENTER_MORE_ARTISTS
1628    DE  Interpret eingeben
1629    EN  Enter Artist Search
1630    ES  Ingresar BC:squeda de Artista
1631    DA  Indtast kunstner
1632    NL  Artiest zoekopdracht
1633
1634LINE2_ENTER_MORE_ALBUMS
1635    DE  Album eingeben
1636    EN  Enter Album Search
1637    ES  Ingresar BC:squeda de Clbumes
1638    DA  Indtast album
1639    NL  Album zoekopdracht
1640
1641LINE2_ENTER_MORE_TRACKS
1642    DE  Titel eingeben
1643    EN  Enter Song Search
1644    ES  Ingresar BC:squeda de Canciones
1645    DA  Indtast sang
1646    NL  Liedjes zoekopdracht
1647
1648LINE2_ENTER_MORE_GENRES
1649    DE  Stilrichtung eingeben
1650    EN  Enter Genre Search
1651    ES  Ingresar BC:squeda de GC)neros
1652    DA  Indtast genre
1653    NL  Genre zoekopdracht
1654
1655SETUP_GROUP_PLUGIN_LAZYSEARCH2
1656    DE  Faulpelz-Suche
1657    EN  Lazy Search
1658    ES  BC:squeda Laxa
1659    DA  Lazy Search
1660    NL  Lazy Search
1661
1662SETUP_GROUP_PLUGIN_LAZYSEARCH2_DESC
1663    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).
1664    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)
1665    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).
1666    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).
1667    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).
1668
1669SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST
1670    DE  MindestlC$nge fC<r die Suche nach Interpreten
1671    EN  Minimum Artist Search Length
1672    ES  MC-nima Longitud para BC:squeda de Artista
1673    DA  Minimum kunstnersC8ge lC&ngde
1674    NL  Minimum Artiest zoekopdracht lengte
1675
1676SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_DESC
1677    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.
1678    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.
1679    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.
1680    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.
1681    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.
1682   
1683SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHOOSE
1684    DE  MindestlC$nge fC<r die Suche nach Interpreten (2-9 Zeichen):
1685    EN  Minimum length for artist search (2-9 characters):
1686    ES  MC-nima longitud para bC:squeda de artista (2-9 caracteres):
1687    DA  Minimum lC&ngde for kunstner sC8gning (2-9 tegn):
1688    NL  De minimumlengte voor Artiest zoekopdracht (2-9 tekens):
1689
1690SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ARTIST_CHANGE
1691    DE  MindestlC$nge fC<r die Suche nach Interpreten wurde geC$ndert in:
1692    EN  Minimum length for artist search changed to:
1693    ES  MC-nima longitud para bC:squeda de artista cambiC3 a:
1694    DA  Minimum lC&ngde for kunstner sC8gning C&ndret til:
1695    NL  De minimumlengte voor Artiest zoekopdracht is gewijzigd in:
1696
1697SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM
1698    DE  MindestlC$nge fC<r die Suche nach Alben
1699    EN  Minimum Album Search Length
1700    ES  MC-nima Longitud para BC:squeda de Clbum
1701    DA  Minimum lC&ngde for album sC8gning
1702    NL  Minimum Album zoekopdracht lengte
1703
1704SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHOOSE
1705    DE  MindestlC$nge fC<r die Suche nach Alben (2-9 Zeichen):
1706    EN  Minimum length for album search (2-9 characters):
1707    ES  MC-nima longitud para bC:squeda de C!lbum (2-9 caracteres):
1708    DA  Minimum lC&ngde for album sC8gning (2-9 tegn):
1709    NL  De minimumlengte voor Album zoekopdracht (2-9 tekens):
1710
1711SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_ALBUM_CHANGE
1712    DE  MindestlC$nge fC<r die Suche nach Alben wurde geC$ndert in:
1713    EN  Minimum length for album search changed to:
1714    ES  MC-nima longitud para bC:squeda de C!lbum cambiC3 a:
1715    DA  Minimum lC&ngde for album sC8gning rettet til:
1716    NL  De minimumlengte voor Album zoekopdracht is gewijzigd in:
1717
1718SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK
1719    DE  MindestlC$nge fC<r die Suche nach Titel
1720    EN  Minimum Song Search Length
1721    ES  MC-nima Longitud para BC:squeda de CanciC3n
1722    DA  Minimum lC&ngde for sang sC8gning
1723    NL  Minimum Liedjes zoekopdracht lengte
1724
1725SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHOOSE
1726    DE  MindestlC$nge fC<r die Suche nach Titel (2-9 Zeichen):
1727    EN  Minimum length for song search (2-9 characters):
1728    ES  MC-nima longitud para bC:squeda de canciC3n (2-9 caracteres):
1729    DA  Minimum lC&ngde for sang sC8gning (2-9 tegn):
1730    NL  De minimumlengte voor Liedjes zoekopdracht (2-9 tekens):
1731
1732SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_TRACK_CHANGE
1733    DE  MindestlC$nge fC<r die Suche nach Titel wurde geC$ndert in:
1734    EN  Minimum length for song search changed to:
1735    ES  MC-nima longitud para bC:squeda de canciC3n cambiC3 a:
1736    DA  Minimum lC&ngde for sang sC8gning rettet til:
1737    NL  De minimumlengte voor Liedjes zoekopdracht is gewijzigd in:
1738
1739SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE
1740    DE  MindestlC$nge fC<r die Suche nach Stilrichtungen
1741    EN  Minimum Genre Search Length
1742    ES  MC-nima Longitud para BC:squeda de GC)nero
1743    DA  Minimum lC&ngde for sang sC8gning
1744    NL  Minimum Genre zoekopdracht lengte
1745
1746SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHOOSE
1747    DE  MindestlC$nge fC<r die Suche nach Stilrichtungen (2-9 Zeichen):
1748    EN  Minimum length for genre search (2-9 characters):
1749    ES  MC-nima longitud para bC:squeda de gC)nero (2-9 caracteres):
1750    DA  Minimum lC&ngde for genre sC8gning (2-9 tegn):
1751    NL  De minimumlengte voor Genre zoektocht (2-9 tekens):
1752
1753SETUP_PLUGIN_LAZYSEARCH2_MINLENGTH_GENRE_CHANGE
1754    DE  MindestlC$nge fC<r die Suche nach Stilrichtungen wurde geC$ndert in:
1755    EN  Minimum length for genre search changed to:
1756    ES  MC-nima longitud para bC:squeda de gC)nero cambiC3 a:
1757    DA  Minimum lC&ngde for genre sC8gning rettet til:
1758    NL  De minimumlengte voor Genre zoektocht is gewijzigd in:
1759
1760SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES
1761    DE  Verhalten der LINKS-Taste
1762    EN  LEFT Button Behaviour
1763    ES  Comportamiento del BotC3n IZQUIERDA
1764    DA  VENSTRE-knap opfC8rsel
1765    NL  LINKERknop Gedrag
1766
1767SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_DESC
1768    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.
1769    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.
1770    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.
1771    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.
1772    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.
1773
1774SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHOOSE
1775    DE  DrC<cken der LINKS-Taste wC$hrend einer Suche:
1776    EN  Pressing LEFT while entering a search:
1777    ES  Presionando IZQUIERDA mientras se ingresa una bC:squeda:
1778    DA  Ved tryk pC% VENSTRE under sC8gning:
1779    NL  LINKERknop gebruikt in zoekmode:
1780
1781SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_CHANGE
1782    DE  DrC<cken der LINKS-Taste wurde geC$ndert in:
1783    EN  Pressing LEFT changed to:
1784    ES  Presionando IZQUIERDA cambiC3 a:
1785    DA  Tyk VENSTRE for at:
1786    NL  LINKERknop gewijzigd in:
1787
1788SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_0
1789    DE  Beendet den Suchmodus
1790    EN  Exits the search mode
1791    ES  Abandona el modo bC:squeda
1792    DA  Forlade sC8gning
1793    NL  Verlaat de zoekmode
1794
1795SETUP_PLUGIN_LAZYSEARCH2_LEFTDELETES_1
1796    DE  LC6scht das zuletzt eingegebene Zeichen
1797    EN  Deletes the last character entered
1798    ES  Borra los C:ltimos caracteres ingresados
1799    DA  Slette det sidst tastede tegn
1800    NL  Verwijder het laatst ingevoerde karakter
1801
1802SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON
1803    DE  Verhalten der SEARCH-Taste
1804    EN  SEARCH Button Behaviour
1805    ES  Comportamiento del BotC3n SEARCH
1806    DA  SEARCH-knap opfC8rsel
1807    NL  ZOEKknop Gedrag
1808
1809SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_DESC
1810    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: Cnderungen an dieser Einstellung werden erst nach einem erneuten Start des Plugins wirksam (z.B. bei einem Neustart des SlimServers).
1811    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).
1812    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).
1813    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).
1814    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).
1815
1816SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHOOSE
1817    DE  DrC<cken der SEARCH-Taste auf der Squeezebox-Fernbedienung:
1818    EN  Pressing SEARCH on the Squeezebox remote:
1819    ES  Presionando SEARCH en el remoto de Squeezebox:
1820    DA  Tryk pC% SEARCH knappen pC% Squeezebox/Transporter fjernbetjC&ningen:
1821    NL  ZOEKknop op de Squeezebox afstandsbediening:
1822
1823SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_CHANGE
1824    DE  DrC<cken der SEARCH-Taste wurde geC$ndert in:
1825    EN  Pressing SEARCH changed to:
1826    ES  Presionando SEARCH cambiC3 a:
1827    DA  Tryk pC% SEARCH gC%r til:
1828    NL  ZOEKknop gewijzigd in:
1829
1830SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_0
1831    DE  Zeigt das MenC< der Standardsuche an
1832    EN  Accesses the standard search music menu
1833    ES  Accede al menC: de bC:squeda musical estC!ndar
1834    DA  Standard sC8gning
1835    NL  Geeft toegang tot het normale zoek muziek menu
1836
1837SETUP_PLUGIN_LAZYSEARCH2_HOOKSEARCHBUTTON_1
1838    DE  Zeigt das MenC< der Faulpelz-Suche an
1839    EN  Accesses the lazy search music menu
1840    ES  Accede al menC: de bC:squeda musical laxa
1841    DA  Lazy Serach menuen.
1842    NL  Geeft toegang tot het Lazy Search muziek menu
1843
1844SCAN_IN_PROGRESS
1845    DE  Hinweis: Die Musikdatenbank wird gerade durchsucht
1846    EN  Note: music library scan in progress
1847    ES  Nota: se estC! recopilando la colecciC3n musical
1848    DA  Note: dit musik biblioteket bliver lige nu scannet.
1849    NL  Merk op: muziek bibliotheek wordt nu ingelezen
1850
1851SCAN_IN_PROGRESS_DBL
1852    DE  Hinweis: Suche lC$uft
1853    EN  Note: scanning
1854    ES  Nota: recopilando
1855    DA  Note: scanner
1856    NL  Merk op: Inlezen...
1857
1858SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW
1859    DE  Indexerzeugung fC<r die Faulpelz-Suche
1860    EN  Force Lazy Search Index Build
1861    ES  Forzar CreaciC3n de C
1862ndice para BC:squeda Laxa
1863    DA  Gennemtving opbygningen af Lazy Seach indexet
1864    NL  Forceer de Lazy Search Indexering
1865
1866SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_DESC
1867    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.
1868    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.
1869    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).
1870    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.
1871    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.
1872
1873SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_CHANGE
1874    DE  Die Erzeugung des Index fC<r die Faulpelz-Suche hat begonnen
1875    EN  Lazy search index build has been started
1876    ES  La creaciC3n del C-ndice para bC:squeda laxa ha comenzado
1877    DA  Lazy Search indexet bliver du genopbygget
1878    NL  Lazy Search indexering is gestart
1879
1880SETUP_PLUGIN_LAZYSEARCH2_LAZIFYNOW_BUTTON
1881    DE  Jetzt den Index fC<r die Faulpelz-Suche erzeugen
1882    EN  Build Lazy Search Index Now
1883    ES  Crear C
1884ndice de BC:squeda Laxa Ahora
1885    DA  Start opbygning af Lazy Search indexet
1886    NL  CreC+er de Lazy Search Index
1887';
1888}
1889
18901;
1891
1892__END__
1893
1894# Local Variables:
1895# tab-width:4
1896# indent-tabs-mode:t
1897# End: