From 288902a572c0ff92b9019cb0f2f633e9c8a4af42 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Thu, 14 Jun 2012 18:47:40 -0400 Subject: [PATCH 1/6] add experimental fuzzy matching plugin fuzzy is a command which tries to be like the list command but using fuzzy matching. --- beetsplug/fuzzy_list.py | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 beetsplug/fuzzy_list.py diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_list.py new file mode 100644 index 000000000..ca409dcdf --- /dev/null +++ b/beetsplug/fuzzy_list.py @@ -0,0 +1,84 @@ +# This file is part of beets. +# Copyright 2011, Philippe Mongeau. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Get a random song or album from the library. +""" +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand, decargs, print_ +from beets.util.functemplate import Template +import random +import difflib + +def fuzzy_score(query, item): + return difflib.SequenceMatcher(a=query, b=item).quick_ratio() + +def is_match(query, item, verbose=False): + query = ' '.join(query) + s = max(fuzzy_score(query, i) for i in (item.artist, + item.album, + item.title)) + if s > 0.7: return (True, s) if verbose else True + else: return (False, s) if verbose else False + +def fuzzy_list(lib, config, opts, args): + query = decargs(args) + path = opts.path + fmt = opts.format + verbose = opts.verbose + + if fmt is None: + # If no specific template is supplied, use a default + if opts.album: + fmt = u'$albumartist - $album' + else: + fmt = u'$artist - $album - $title' + template = Template(fmt) + + if opts.album: + objs = list(lib.albums()) + else: + objs = list(lib.items()) + + # matches = [i for i in objs if is_match(query, i)] + + if opts.album: + for album in objs: + if path: + print_(album.item_dir()) + else: + print_(album.evaluate_template(template)) + else: + for item in objs: + if is_match(query, item): + if path: + print_(item.path) + else: + print_(item.evaluate_template(template, lib)) + if verbose: print is_match(query,item, True)[1] + +fuzzy_cmd = Subcommand('fuzzy', + help='list items using fuzzy matching') +fuzzy_cmd.parser.add_option('-a', '--album', action='store_true', + help='choose an album instead of track') +fuzzy_cmd.parser.add_option('-p', '--path', action='store_true', + help='print the path of the matched item') +fuzzy_cmd.parser.add_option('-f', '--format', action='store', + help='print with custom format', default=None) +fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', + help='output scores for matches') +fuzzy_cmd.func = fuzzy_list + +class Random(BeetsPlugin): + def commands(self): + return [fuzzy_cmd] From 7187d93303b7575511a77416c5e036cef67d9a39 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Thu, 14 Jun 2012 18:55:47 -0400 Subject: [PATCH 2/6] implement album option for the fuzzy plugin --- beetsplug/fuzzy_list.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_list.py index ca409dcdf..adbeb9447 100644 --- a/beetsplug/fuzzy_list.py +++ b/beetsplug/fuzzy_list.py @@ -23,11 +23,13 @@ import difflib def fuzzy_score(query, item): return difflib.SequenceMatcher(a=query, b=item).quick_ratio() -def is_match(query, item, verbose=False): +def is_match(query, item, album=False, verbose=False): query = ' '.join(query) - s = max(fuzzy_score(query, i) for i in (item.artist, - item.album, - item.title)) + + if album: values = [item.albumartist, item.album] + else: values = [item.artist, item.album, item.title] + + s = max(fuzzy_score(query, i) for i in values) if s > 0.7: return (True, s) if verbose else True else: return (False, s) if verbose else False @@ -54,10 +56,12 @@ def fuzzy_list(lib, config, opts, args): if opts.album: for album in objs: - if path: - print_(album.item_dir()) - else: - print_(album.evaluate_template(template)) + if is_match(query, album, album=True): + if path: + print_(album.item_dir()) + else: + print_(album.evaluate_template(template)) + if verbose: print is_match(query,album, album=True, verbose=True)[1] else: for item in objs: if is_match(query, item): @@ -65,7 +69,7 @@ def fuzzy_list(lib, config, opts, args): print_(item.path) else: print_(item.evaluate_template(template, lib)) - if verbose: print is_match(query,item, True)[1] + if verbose: print is_match(query,item, verbose=True)[1] fuzzy_cmd = Subcommand('fuzzy', help='list items using fuzzy matching') From 5a2719711e8b3f1c7656c3c922c434d43c89db7e Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 18 Jun 2012 17:28:10 -0400 Subject: [PATCH 3/6] Rename Fuzzy plugin class name. remove useless conversion of an iterator to a list the plugin class was called Random (because of copy paste) --- beetsplug/fuzzy_list.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_list.py index adbeb9447..e124058f8 100644 --- a/beetsplug/fuzzy_list.py +++ b/beetsplug/fuzzy_list.py @@ -48,9 +48,9 @@ def fuzzy_list(lib, config, opts, args): template = Template(fmt) if opts.album: - objs = list(lib.albums()) + objs = lib.albums() else: - objs = list(lib.items()) + objs = lib.items() # matches = [i for i in objs if is_match(query, i)] @@ -83,6 +83,8 @@ fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') fuzzy_cmd.func = fuzzy_list -class Random(BeetsPlugin): + + +class Fuzzy(BeetsPlugin): def commands(self): return [fuzzy_cmd] From ca237ce3e74e24c9e9e8429c568926bfd63c986c Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 17 Sep 2012 22:10:36 -0400 Subject: [PATCH 4/6] make fuzzy_search case insensitive and add a threshold option renamed fuzzy_list.py to fuzzy_search.py --- beetsplug/{fuzzy_list.py => fuzzy_search.py} | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) rename beetsplug/{fuzzy_list.py => fuzzy_search.py} (78%) diff --git a/beetsplug/fuzzy_list.py b/beetsplug/fuzzy_search.py similarity index 78% rename from beetsplug/fuzzy_list.py rename to beetsplug/fuzzy_search.py index e124058f8..064125320 100644 --- a/beetsplug/fuzzy_list.py +++ b/beetsplug/fuzzy_search.py @@ -12,25 +12,26 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Get a random song or album from the library. +"""Like beet list, but with fuzzy matching """ from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_ from beets.util.functemplate import Template -import random import difflib +# THRESHOLD = 0.7 + def fuzzy_score(query, item): return difflib.SequenceMatcher(a=query, b=item).quick_ratio() -def is_match(query, item, album=False, verbose=False): +def is_match(query, item, album=False, verbose=False, threshold=0.7): query = ' '.join(query) if album: values = [item.albumartist, item.album] else: values = [item.artist, item.album, item.title] - s = max(fuzzy_score(query, i) for i in values) - if s > 0.7: return (True, s) if verbose else True + s = max(fuzzy_score(query.lower(), i.lower()) for i in values) + if s > threshold: return (True, s) if verbose else True else: return (False, s) if verbose else False def fuzzy_list(lib, config, opts, args): @@ -38,6 +39,7 @@ def fuzzy_list(lib, config, opts, args): path = opts.path fmt = opts.format verbose = opts.verbose + threshold = float(opts.threshold) if fmt is None: # If no specific template is supplied, use a default @@ -52,19 +54,18 @@ def fuzzy_list(lib, config, opts, args): else: objs = lib.items() - # matches = [i for i in objs if is_match(query, i)] - if opts.album: for album in objs: - if is_match(query, album, album=True): + if is_match(query, album, album=True, threshold=threshold): if path: print_(album.item_dir()) else: print_(album.evaluate_template(template)) - if verbose: print is_match(query,album, album=True, verbose=True)[1] + if verbose: print is_match(query, album, + album=True, verbose=True)[1] else: for item in objs: - if is_match(query, item): + if is_match(query, item, threshold=threshold): if path: print_(item.path) else: @@ -81,6 +82,9 @@ fuzzy_cmd.parser.add_option('-f', '--format', action='store', help='print with custom format', default=None) fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') +fuzzy_cmd.parser.add_option('-t', '--threshold', action='store', + help='return result with a fuzzy score above threshold. (default is 0.7)', + default=0.7) fuzzy_cmd.func = fuzzy_list From dfca295e31990403e3b205680b4c8676edeaa772 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 17 Sep 2012 22:23:15 -0400 Subject: [PATCH 5/6] pep8ify fuzzy_search --- beetsplug/fuzzy_search.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/beetsplug/fuzzy_search.py b/beetsplug/fuzzy_search.py index 064125320..4e93a4ee3 100644 --- a/beetsplug/fuzzy_search.py +++ b/beetsplug/fuzzy_search.py @@ -21,18 +21,25 @@ import difflib # THRESHOLD = 0.7 + def fuzzy_score(query, item): return difflib.SequenceMatcher(a=query, b=item).quick_ratio() + def is_match(query, item, album=False, verbose=False, threshold=0.7): query = ' '.join(query) - if album: values = [item.albumartist, item.album] - else: values = [item.artist, item.album, item.title] + if album: + values = [item.albumartist, item.album] + else: + values = [item.artist, item.album, item.title] + + s = max(fuzzy_score(query.lower(), i.lower()) for i in values) + if s >= threshold: + return (True, s) if verbose else True + else: + return (False, s) if verbose else False - s = max(fuzzy_score(query.lower(), i.lower()) for i in values) - if s > threshold: return (True, s) if verbose else True - else: return (False, s) if verbose else False def fuzzy_list(lib, config, opts, args): query = decargs(args) @@ -61,8 +68,8 @@ def fuzzy_list(lib, config, opts, args): print_(album.item_dir()) else: print_(album.evaluate_template(template)) - if verbose: print is_match(query, album, - album=True, verbose=True)[1] + if verbose: + print is_match(query, album, album=True, verbose=True)[1] else: for item in objs: if is_match(query, item, threshold=threshold): @@ -70,7 +77,8 @@ def fuzzy_list(lib, config, opts, args): print_(item.path) else: print_(item.evaluate_template(template, lib)) - if verbose: print is_match(query,item, verbose=True)[1] + if verbose: + print is_match(query, item, verbose=True)[1] fuzzy_cmd = Subcommand('fuzzy', help='list items using fuzzy matching') @@ -83,12 +91,11 @@ fuzzy_cmd.parser.add_option('-f', '--format', action='store', fuzzy_cmd.parser.add_option('-v', '--verbose', action='store_true', help='output scores for matches') fuzzy_cmd.parser.add_option('-t', '--threshold', action='store', - help='return result with a fuzzy score above threshold. (default is 0.7)', - default=0.7) + help='return result with a fuzzy score above threshold. \ + (default is 0.7)', default=0.7) fuzzy_cmd.func = fuzzy_list - class Fuzzy(BeetsPlugin): def commands(self): return [fuzzy_cmd] From a49dcb81c67861956e44cc82f575f20efe4679d1 Mon Sep 17 00:00:00 2001 From: Philippe Mongeau Date: Mon, 17 Sep 2012 22:41:02 -0400 Subject: [PATCH 6/6] add documentation for fuzzy_search --- docs/plugins/fuzzy_search.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/plugins/fuzzy_search.rst diff --git a/docs/plugins/fuzzy_search.rst b/docs/plugins/fuzzy_search.rst new file mode 100644 index 000000000..e0ecffc7f --- /dev/null +++ b/docs/plugins/fuzzy_search.rst @@ -0,0 +1,20 @@ +Fuzzy Search Plugin +============= + +The ``fuzzy_search`` plugin provides a command that search your library using +fuzzy pattern matching. This can be useful if you want to find a track with complicated characters in the title. + +First, enable the plugin named ``fuzzy_search`` (see :doc:`/plugins/index`). +You'll then be able to use the ``beet fuzzy`` command:: + + $ beet fuzzy Vareoldur + Sigur Rós - Valtari - Varðeldur + +The command has several options that resemble those for the ``beet list`` +command (see :doc:`/reference/cli`). To choose an album instead of a single +track, use ``-a``; to print paths to items instead of metadata, use ``-p``; and +to use a custom format for printing, use ``-f FORMAT``. + +The ``-t NUMBER`` option lets you specify how precise the fuzzy match has to be +(default is 0.7). To make a fuzzier search, try ``beet fuzzy -t 0.5 Varoeldur``. +A value of ``1`` will show only perfect matches and a value of ``0`` will match everything.