Merge branch 'Emms-filters' - emms.git - EMMS, The Emacs Multimedia System.

index : emms.git
EMMS, The Emacs Multimedia System.
summary refs log tree commit diff
diff options
context:
space:
mode:
authorYoni Rabkin <yrk@gnu.org>2025年07月12日 17:27:02 -0400
committerYoni Rabkin <yrk@gnu.org>2025年07月12日 17:27:02 -0400
commit62eca2f5ee9e100564223a676014bf3d29efb31f (patch)
treef85b74ad88f9f1fe387342faf5cc0d43cb616914
parente5f46561c3c140d774354f91d1e95c60039a1934 (diff)
parentbd5e088932b64f499e4f51c53f176abd4b859895 (diff)
downloademms-62eca2f5ee9e100564223a676014bf3d29efb31f.tar.gz
Merge branch 'Emms-filters'
Diffstat
-rw-r--r--doc/emms.texinfo 1068
-rw-r--r--emms-browser.el 585
-rw-r--r--emms-filters.el 2119
3 files changed, 3423 insertions, 349 deletions
diff --git a/doc/emms.texinfo b/doc/emms.texinfo
index 8a2c799..1730de3 100644
--- a/doc/emms.texinfo
+++ b/doc/emms.texinfo
@@ -72,6 +72,7 @@ Advanced Features
Modules and Extensions
* The Browser:: Advanced metadata browsing.
+* The Filter System:: Advanced metadata filtering.
* Sorting Playlists:: Sorting the order of the tracks.
* Persistent Playlists:: Restoring playlists on emacs startup.
* Editing Tracks:: Editing track information from within Emms.
@@ -1832,9 +1833,9 @@ once so that the cache is fully populated.
@menu
* Browser Interface:: The interactive browser interface.
-* Filtering Tracks:: Displaying a subset of the tracks.
* Displaying Covers:: Displaying album covers in the browser interface.
* Changing Looks:: Changing the tree structure, display format and faces.
+* Filtering Tracks - deprecated:: Displaying a subset of the tracks.
@end menu
@@ -1868,6 +1869,14 @@ Display the browser and order the tracks by genre.
Display the browser and order the tracks by year.
@end defun
+@defun emms-browser-show-searches
+Show Search crumbs of the active searches.
+@end defun
+
+@defun emms-browser-render-last-search
+Render the results for the last search with current settings.
+@end defun
+
Once the Browser is displayed you can use it to managed your track
collection and playlists. The Browser is interactive and has its own
keybindings.
@@ -1941,12 +1950,12 @@ Isearch through the buffer.
@item <
@kindex < (emms-browser)
-@findex emms-browser-previous-filter
+@findex emms-filters-previous-ring-filter
Redisplay with the previous filter.
@item >
@kindex > (emms-browser)
-@findex emms-browser-next-filter
+@findex emms-filters-next-ring-filter
Redisplay with the next filter.
@item ?
@@ -1979,6 +1988,11 @@ Jump to the next non-track element.
@findex emms-browser-search-by-album
Search the collection by album.
+@item s o
+@kindex s o (emms-browser)
+@findex emms-browser-search-by-albumartist
+Search the collection by artist.
+
@item s a
@kindex s a (emms-browser)
@findex emms-browser-search-by-artist
@@ -1994,6 +2008,11 @@ Search the collection by names.
@findex emms-browser-search-by-title
Search the collection by title.
+@item s h
+@kindex s h (emms-browser)
+@findex emms-browser-show-searches
+Show the currently active searches in the search cache.
+
@item b 1
@kindex b 1 (emms-browser)
@findex emms-browse-by-artist
@@ -2014,89 +2033,279 @@ Browse the collection by genre.
@findex emms-browse-by-year
Browse the collection by year.
+@item b 5
+@kindex b 5 (emms-browser)
+@findex emms-browse-by-composer
+Browse the collection by composer.
+
+@item b 6
+@kindex b 6 (emms-browser)
+@findex emms-browse-by-performer
+Browse the collection by performer.
+
+@item b 7
+@kindex b 5 (emms-browser)
+@findex emms-browse-by-albumartist
+Browse the collection by albumartist.
+
@item W a p
@kindex W a p (emms-browser)
@findex emms-browser-lookup-album-on-pitchfork
Lookup the album using Pitchfork.
-@item W a w
-@kindex W a w (emms-browser)
-@findex emms-browser-lookup-album-on-wikipedia
-Lookup the album using Wikipedia.
-@end table
-
-
-
-@node Filtering Tracks
-@section Filtering Tracks
+@item W o w
+@kindex W o w (emms-browser)
+@findex emms-browser-lookup-albumartist-on-wikipedia
+Lookup the album artist using Wikipedia.
-If you want to display a subset of your collection (such as a
-directory of 80s music, only avi files, etc.) then you can extend the
-Browser by defining ``filters''.
+@item W A w
+@kindex W A w (emms-browser)
+@findex emms-browser-lookup-artist-on-wikipedia
+Lookup the artist using Wikipedia.
-Show everything:
+@item W c w
+@kindex W c w (emms-browser)
+@findex emms-browser-lookup-composer-on-wikipedia
+Lookup the composer using Wikipedia.
-@lisp
-(emms-browser-make-filter "all" 'ignore)
-@end lisp
+@item W p w
+@kindex W p w (emms-browser)
+@findex emms-browser-lookup-performer-on-wikipedia
+Lookup the performer using Wikipedia.
-Set "all" as the default filter:
+@item W a w
+@kindex W a w (emms-browser)
+@findex emms-browser-lookup-album-on-wikipedia
+Lookup the album using Wikipedia.
-@lisp
-(emms-browser-set-filter (assoc "all" emms-browser-filters))
-@end lisp
+@item +
+@kindex + (emms-browser)
+@findex emms-volume-raise
+Raise the volume
+
+@item -
+@kindex - (emms-browser)
+@findex emms-volume-lower
+Lower the volume
+
+@item i s
+@kindex i s (emms-browser)
+@findex emms-filters-status-print
+Print what is known about the filters and cache
+
+@item i c
+@kindex i c (emms-browser)
+@findex emms-filters-show-cache-stack
+Show the current search cache stack.
+
+@item i S
+@kindex i S (emms-browser)
+@findex emms-filters-show-cache-stash
+Show the cache names in the stash.
+
+@item i f
+@kindex i f (emms-browser)
+@findex emms-filters-show-filters
+Show the filters there are.
+
+@item i m
+@kindex i m (emms-browser)
+@findex emms-filters-show-filter-menu
+Show the menu tree of filters as a menu.
+
+@item i F
+@kindex i F (emms-browser)
+@findex emms-filters-show-filter-factories
+Show the filter factories we have.
+
+@item i r
+@kindex i r (emms-browser)
+@findex emms-filters-show-filter-ring
+Show the filters in the filter ring.
+
+@item f q
+@kindex f q (emms-browser)
+@findex emms-filters-pop
+Pop the filter stack returning to last filter.
+
+@item f r
+@kindex f r
+@findex emms-filters-swap (emms-browser)
+Reverse the last two entries in the filter stack.
+
+@item f R
+@kindex f R (emms-browser)
+@findex emms-filters-swap-pop ; rotate-eject, ,pop-previous
+Reverse the last two entries in the filter stack, and pop the top one.
+
+@item f S
+@kindex f S (emms-browser)
+@findex emms-filters-squash
+Squash the filter stack, keep the top entry.
+
+@item f k
+@kindex f k (emms-browser)
+@findex emms-filters-keep
+Register the current filter into the list of filters for the session.
+If @var{emms-filters-multi-filter-save-file} is set, append the filter definition there.
+
+@item f h
+@kindex f h (emms-browser)
+@findex emms-filters-hard-filter
+Build a cache from the current filter and push it to the cache stack.
+
+@item f c
+@kindex f c (emms-browser)
+@findex emms-filters-clear
+Clear the meta filter stack and the current filter function.
-Show all files (no streamlists, etc):
+@item >
+@kindex > (emms-browser)
+@findex emms-filters-next-ring-filter
+Move to the next filter in the filter ring.
-@lisp
-(emms-browser-make-filter
- "all-files" (emms-browser-filter-only-type 'file))
-@end lisp
+@item <
+@kindex < (emms-browser)
+@findex emms-filters-previous-ring-filter
+Move to the previous filter in the filter ring.
+
+@item f !
+@kindex f ! (emms-browser)
+@findex emms-filters-clear-ring-filter
+Set the ring filter to no filter.
+
+@item f p
+@kindex f p (emms-browser)
+@findex emms-filters-push
+Push a filter to the meta-filter stack.
+
+@item f s
+@kindex f s (emms-browser)
+@findex emms-filters-smash
+Clear the stack and select a filter to push to the stack.
+
+@item f o
+@kindex f o (emms-browser)
+@findex emms-filters-or
+Add a filter to the current/last filter list in the current filter.
+Creates an OR.
+
+@item f a
+@kindex f a (emms-browser)
+@findex emms-filters-and
+Select a filter to start a new list of filters, creates an AND.
+
+@item f n
+@kindex f n (emms-browser)
+@findex emms-filters-and-not
+Select a filter to start a new list of filters, creates an AND-NOT list of filters.
+
+@item c C
+@kindex c C (emms-browser)
+@findex emms-filters-clear-all
+Reset the cache stack, the filter stack and the filter-ring.
+
+@item c p
+@kindex c p (emms-browser)
+@findex emms-filters-push-cache
+Cache/Store a filter and cache to the stack.
+
+@item c z
+@kindex c z (emms-browser)
+@findex emms-filters-stash-pop-cache
+Stash the current cache for later, pop it from the stack.
+
+@item c Z
+@kindex c Z (emms-browser)
+@findex emms-filters-stash-cache
+Stash the current cache for later.
+
+@item c P
+@kindex c P (emms-browser)
+@findex emms-filters-pop-cache
+Pop the current cache from the stack.
+
+@item c h
+@kindex c h (emms-browser)
+@findex emms-filters-hard-filter
+Create a cache from the current filter and push to the stack.
+
+@item c r
+@kindex c r (emms-browser)
+@findex emms-filters-swap-cache
+Swap the top two caches on the stack.
+
+@item c q
+@kindex c q (emms-browser)
+@findex emms-filters-pop-cache
+Pop the top cache from the stack.
+
+@item c R
+@kindex c R (emms-browser)
+@findex emms-filters-swap-pop-cache
+Swap the top two caches on the stack, then pop the top one.
+
+@item c S
+@kindex c S (emms-browser)
+@findex emms-filters-squash-caches
+Squash the cache stack, keep the top entry.
+
+@item c c
+@kindex c c (emms-browser)
+@findex emms-filters-clear-caches
+Clear all the caches down to the main cache.
+
+@item s o
+@kindex s o (emms-browser)
+@findex emms-filters-search-by-albumartist
+A fields search, quick one-shot for Album artist, push results to the cache stack.
-Show only tracks in one folder:
+@item s a
+@kindex s a (emms-browser)
+@findex mf-search-by-artist
+A fields search, quick one-shot for Artist, push results to the cache stack.
-@lisp
-(emms-browser-make-filter
- "80s" (emms-browser-filter-only-dir "~/Mp3s/80s"))
-@end lisp
+@item s c
+@kindex s c (emms-browser)
+@findex emms-filters-search-by-composer
+A fields search, quick one-shot for Composer, push results to the cache stack.
-Show all tracks played in the last month:
+@item s p
+@kindex s p (emms-browser)
+@findex emms-filters-search-by-performer
+A fields search, quick one-shot for Permformer, push results to the cache stack.
-@lisp
-(emms-browser-make-filter
- "last-month" (emms-browser-filter-only-recent 30))
-@end lisp
+@item s A
+@kindex s A (emms-browser)
+@findex emms-filters-search-by-album
+A fields search, quick one-shot for Album title, push results to the cache stack.
-After executing the above commands, you can use M-x
-emms-browser-show-all, emms-browser-show-80s, etc to toggle between
-different collections. Alternatively you can use '<' and '>' to cycle
-through the available filters.
+@item s t
+@kindex s t (emms-browser)
+@findex emms-filters-search-by-title
+A fields search, quick one-shot for Song title, push results to the cache stack.
-The second argument to make-filter is a function which returns t if a
-single track should be filtered. You can write your own filter
-functions to check the type of a file, etc.
+@item s T
+@kindex s T (emms-browser)
+@findex emms-filters-search-by-titles
+A fields search, quick one-shot for Album and song titles, push results to the cache stack.
-Show only tracks not played in the last year:
+@item s n
+@kindex s n (emms-browser)
+@findex emms-filters-search-by-names
+A fields search, quick one-shot for all names, push results to the cache stack.
-@lisp
-(emms-browser-make-filter "not-played"
- (lambda (track)
- (not (funcall (emms-browser-filter-only-recent 365) track))))
-@end lisp
-
-Show all files that are not in the pending directory:
-
-@lisp
-(emms-browser-make-filter
- "all"
- (lambda (track)
- (or
- (funcall (emms-browser-filter-only-type 'file) track)
- (not (funcall
- (emms-browser-filter-only-dir "~/Media/pending") track)))))
-@end lisp
+@item s s
+@kindex s s (emms-browser)
+@findex emms-filters-search-by-names-and-title
+A fields search, quick one-shot for all names and titles, push results to the cache stack.
+@item s e
+@kindex s e (emms-browser)
+@findex emms-filters-search-by-all-text
+A fields search, quick one-shot in all text fields, push results to the cache stack.
+@end table
@node Displaying Covers
@section Displaying Covers
@@ -2157,8 +2366,29 @@ structure looks, the display format and display faces.
@subheading Changing Tree Structure
-You can change the way the tree is displayed by modifying the function
-@command{emms-browser-next-mapping-type}.
+You can change the way the tree is displayed by setting the value of
+@var{emms-browser-tree-node-map}
+
+@lisp
+(setq emms-browser-tree-node-map emms-browser-tree-node-map-default)
+@end lisp
+
+The node map specifies the tree as an alist.
+Follow the chain of fields from the starting browse type
+to see how the browser tree will be built. ie. Starting with
+album artist yields a tree with album artist, artist and album as nodes.
+
+@lisp
+(defvar emms-browser-tree-node-map-default
+ '((info-albumartist . info-artist)
+ (info-artist . info-album)
+ (info-composer . info-album)
+ (info-performer . info-album)
+ (info-album . info-title)
+ (info-genre . info-artist)
+ (info-year . info-artist)))
+@end lisp
+
The following code displays artist->track instead of
artist->album->track when you switch to the 'singles' filter:
@@ -2176,7 +2406,9 @@ artist->album->track when you switch to the 'singles' filter:
(ad-activate 'emms-browser-next-mapping-type)
(ad-deactivate 'emms-browser-next-mapping-type)))
-(add-hook 'emms-browser-filter-changed-hook 'toggle-album-display)
+;; Deprecated use the emms-filters hook instead.
+;; (add-hook 'emms-browser-filter-changed-hook 'toggle-album-display)
+(add-hook 'emms-filters-filter-changed-hook 'toggle-album-display)
@end lisp
Furthermore, you can customize
@@ -2226,10 +2458,16 @@ The format specifiers available include:
@item
%a the artist name of the track
+%o the album artist name
+@item
+
@item
%t the title of the track
@item
+%o the genre of the track
+
+@item
%T the track number
@item
@@ -2253,6 +2491,698 @@ They are in the format emms-browser-<type>-face, where type is one of
initial "info-" part. For example, to change the artist face, type M-x
@command{customize-face} @command{emms-browser-artist-face}.
+@node Filtering Tracks - deprecated
+@section Filtering Tracks - deprecated
+
+Note that these are the directions for creating filters from the browser
+API which now works through the Emms-filters system.
+
+The Emms-filter system obsoletes this method of creating filters.
+However, it is still valid, and will work if you have filters defined
+in this way.
+
+It is recommended that the new filter system is used directly rather
+than building filters in this way.
+
+If you want to display a subset of your collection (such as a
+directory of 80s music, only avi files, etc.) then you can extend the
+Browser by defining ``filters''.
+
+Show everything:
+
+@lisp
+(emms-browser-make-filter "all" 'ignore)
+@end lisp
+
+Set "all" as the default filter:
+
+@lisp
+(emms-browser-set-filter (assoc "all" emms-browser-filters))
+@end lisp
+
+Show all files (no streamlists, etc):
+
+@lisp
+(emms-browser-make-filter
+ "all-files" (emms-browser-filter-only-type 'file))
+@end lisp
+
+Show only tracks in one folder:
+
+@lisp
+(emms-browser-make-filter
+ "80s" (emms-browser-filter-only-dir "~/Mp3s/80s"))
+@end lisp
+
+Show all tracks played in the last month:
+
+@lisp
+(emms-browser-make-filter
+ "last-month" (emms-browser-filter-only-recent 30))
+@end lisp
+
+After executing the above commands, you can use M-x
+emms-browser-show-all, emms-browser-show-80s, etc to toggle between
+different collections. Alternatively you can use '<' and '>' to cycle
+through the available filters.
+
+The second argument to make-filter is a function which returns t if a
+single track should be filtered. You can write your own filter
+functions to check the type of a file, etc.
+
+Show only tracks not played in the last year:
+
+@lisp
+(emms-browser-make-filter "not-played"
+ (lambda (track)
+ (not (funcall (emms-browser-filter-only-recent 365) track))))
+@end lisp
+
+Show all files that are not in the pending directory:
+
+@lisp
+(emms-browser-make-filter
+ "all"
+ (lambda (track)
+ (or
+ (funcall (emms-browser-filter-only-type 'file) track)
+ (not (funcall
+ (emms-browser-filter-only-dir "~/Media/pending") track)))))
+@end lisp
+
+
+@c -------------------------------------------------------------------
+@node The Filter System
+@chapter The Filter System
+
+The filter system allows you to filter the metadata cache in order search
+and narrow your track data. It is based on a very powerful interactive
+system consistenting of filter and cache stacks which allow
+the creation and manipulation of complex filters and results caches.
+
+The Filter system is defined in @file{emms-filters.el} and is included as
+part of the Emms-Browser.
+
+Emms-Filters allows you to filter and search the metadata cache.
+This manages the search and filter functionalities of emms-browser.
+
+@menu
+* Filters and Searches:: A simple overview of the filter system.
+* Backward Compatibility:: Backward compatibility with the Emms Browser.
+* Filter Components:: Definitions of basic terminology used by the filter system.
+* Filter Factories:: Defining filter factories.
+* Factory Registration:: Registering Factories and using the prompting system.
+* Defined Factories:: The built-in factories available
+* Defining Filters:: Defining filters.
+* The Filter Stack:: The filter stack, how it works, how to use it.
+* The Cache Stack:: The cache stack, how to use it.
+* Showing State:: Showing the current state of the filter system.
+@end menu
+
+@node Filters and Searches
+@section Filters and Searches
+
+There is little difference between filtering and searching. Searching
+simply results in a data-cache which is smaller than the original.
+
+The filter system has a cache stack where cached results are kept. All
+subsequent filters and searches use the most current cache.
+
+Filtering results in a rendered view for a subset of what is in the
+data-cache. Complex filters are built interactively on a filter stack
+by combining existing filters and interactively created filters.
+
+A small group of filter factories are used to create filter functions
+which are defined entirely as data. Those filters can be combined with
+each other and with interactively created filters on the filter stack
+with OR, AND, as well as AND-NOT. These more complex filters may also
+be saved and coded entirely as data. In addition the filter stack has
+various commands to manipulate it. Pop, swap, swap-pop, squash and clear,
+among others.
+
+For the concept of searches there is a search cache stack which is a
+stack of emms-cache-db hash tables. The emms-cache-db is always present at
+the base of this stack. Any filtered result may be pushed to this stack
+at any time. Filters always operate against the cache at the top of the
+stack. A cache may be stashed for later, and the usual stack manipulation
+functions exist. Pop, swap, swap-pop, squash, clear, stash, and push.
+
+Factories define filters from data and interactively.
+Factories are kept in a ring, and each has a ring of its filters.
+Interactively creating a new filter adds it to the ring for the session.
+Personal filter rings can also be easily added to the filter menu ring.
+
+There is another filter ring, which can have any filters you like, and is
+selectable with < and >. The active filter on the ring combines with
+the filter stack to show the rendered results.
+
+@node Backward Compatibility
+@section Backward Compatibility
+
+Maintaining backward compatibily with the Emmms-browser's previous
+functionality was a prime goal in creating the filter system. Very little
+is left in common, but the Browser's search-by and filtering API remains
+intact and continues to behave as before. Anyone who has made filter
+functions will notice no change in behavior other than there is more
+flexibility in using their functions.
+
+The filter system replaces both emms-browser filters and search-by
+functionalities. The Emms-browser API remains in place, however underneath
+the API it uses the filter system for all of its purposes.
+
+Emms-browser-make-filter and emms-browser-search-by use emms-filters
+for their current functionality. The search-by functionality is quite
+simple. Emulating the browser filters was more complicated and has a
+thin compatibility layer in @file{emms-browser.el}.
+
+In all cases, obtaining the same functionalities soley within emms-filters is
+simpler and and more powerful.
+
+Emms-browser-filter functions are specified to return an
+inverted value. the old @command{emms-browser-make-filter}
+had a slightly different mechanism from the filter system's
+@command{emms-filters-make-filter}. emms-browser-make-filter has been modified to
+pass its filters to the emms-filter system. Those filters will be properly
+inverted and added to the known emms-filters-filters and to the emms-filters-filter-ring
+which emulates the original browser filter ring. This provides a
+seamless experience for previous users of emms-browser filtering. As
+the @var{emms-filters-filter-ring} is functionally equivalent to the browser's
+filter ring.
+
+The browser's 'Search-by' was just one filter factory, which corresponds
+to the filter system's 'fields search' factory, and searches are not
+inverted. The only real difference between the browser's filter and a
+search was that a filter was rendered and a search was saved to a hash
+for subsequent filtering. Filters couldn't filter a search, and neither
+could work against anything but the Emms-cache-db. The equivalent to the
+emms-browser search-by is just a one shot interactive 'new fields-search'
+filter factory that saves a cache and then removes itself.
+
+Emms-Filters is agnostic about the renderer. Currently there has been a
+lot of effor to maintain backward compatibity with the Emms-browser as
+its functionality was replaced. There are the following hooks that any
+renderer could use in order to leverage Emms-Filters.
+
+To maintain independence there are three hook variables which allow
+emms-filters to interact with the Emms-Browsers functionality.
+
+The first is a defcustom hook to mirror the browser's deprecated hook
+of the same name.
+The second hook happens just after, and is for any renderer
+that wishes to re-render when a filter changes.
+The third hook is to tell any renderer to expand its render if there is
+a filter or cache stack entry present.
+
+This a defcustom hook that is run anytime the filters change
+@var{emms-filters-filter-changed-hook}
+
+@lisp
+(add-hook 'emms-filters-filter-changed-hook 'my-filters-have-changed-function)
+@end lisp
+
+The following two hooks are for the renderers which is currently
+just the Emms-Browser. These hooks are the mechanism used to
+actually filter and render the tracks.
+
+When the filter or cache changes Emms-Filters needs to
+tell the renderer to re-build its hash and display it.
+For this purpose there is another hook, the
+@var{emms-filters-make-and-render-hash-hook}.
+
+The Emms-browser function for this is emms-browse-by.
+This function applies the filters, creates a hash,
+and then populates and renders a tree of data.
+@lisp
+(add-hook 'emms-filters-make-and-render-hash-hook 'emms-browse-by)
+@end lisp
+
+The last hook is the @var{emms-filters-expand-render-hook}.
+This is just so that Emms-Filters can tell the renderer to
+expand its tree when there is a filter or cache stack present
+and something has changed.
+For Emms-Browser this is the function emms-browser-expand-all
+
+@lisp
+;; (add-hook 'emms-filters-expand-render-hook 'emms-browser-expand-all)
+@end lisp
+
+The filter system is much more powerful than the previous system of
+filtering and searching and is much easier to use both in code and interactively
+while searching your tracks.
+
+Here is a summary of differences and features of the filter system.
+
+@itemize @bullet
+@item Filters, no matter the complexity, are defined entirely as data.
+@item Filters can be combined with AND, OR as well as AND-NOT.
+@item Filters return true if they match the tracks.
+@item Filters are lambda functions created with factories from data.
+@item There is no difference between a search function and a filter function.
+@item The factory should wrap the lambda in a let with lexical-binding t.
+@item The factories and the filters must both be registered with Emms-filters.
+@item Registered factories have a built in interactive prompting system.
+@item Any results can be pushed to the cache stack for future filters and searches.
+@item Complex filters are created interactively on the filter stack.
+@item Searches are interactively created filters which leave a cache on the stack.
+@item Interactively created filters can be saved as data for later use.
+@item Interactively created filters remain in the filter selection menu for the session.
+@end itemize
+
+@node Filter Components
+@section Filter Components
+-------------------------------------------------------------------
+The filter system consists of a few different mechanisms.
+There are factories to make filters. There is the filter stack
+to manage the creation and use of filters. Filters can be made of filters.
+
+There is the cache stack to handle the saving of a set of filtered results
+into a reduced database cache for subsequent filters.
+
+There is the filter ring for quickly switching between commonly used filters.
+This filter is combined with the current filter stack to render results.
+
+@itemize @bullet
+@item Filter Factories - To make filter functions.
+@item Filters - Defined as data. Dynamically created lambda functions.
+@item Filter menu - A customizable ring of factories and their rings of filters.
+@item Multi-filter - A filter factory to create filters made of filters.
+@item Meta-filter - A multi-filter data definition.
+@item The filter stack - A meta-filter manipulator and multi-filter creator.
+@item The cache stack - A stack of database caches.
+@item The filter ring - A subset of convenient to use filters.
+@end itemize
+
+@node Filter Factories
+@section Filter Factories
+-------------------------------------------------------------------
+Filter factories make filters which are simply test functions which
+take a track and return true or false.
+
+Factories are registered with the Emms-filter system so that they have
+names that can be referenced later. Additionally, registration includes a
+prompt and parameter definition. This allows the Emms-filters prompting
+system to provide an interactive interface to any filter factory in
+order to create new filters at any time.
+
+Filter factories depend upon lexical context of their parameters. In
+order to have data values that stick after function creation there
+is let with lexical-binding to ensure the factory behaves as expected.
+This transfers the values to local values and uses them as normal
+within the returned #'(lambda (track)...) anonymous function.
+
+As an example, here is the generic field-compare function.
+It takes an operator function, a field name and the value to compare.
+This single function can be a new factory for any data field
+using any comparison function we would like.
+
+@lisp
+(defun emms-filters-make-filter-field-compare (operator-func field compare-val)
+ "Make a filter that compares FIELD to COMPARE-VALUE with OPERATOR-FUNC.
+Works for number fields and string fields provided the appropriate
+type match between values and the comparison function. Partials can
+easily make more specific factory functions from this one."
+ (let ((local-operator operator-func)
+ (local-field field)
+ (local-compare-val compare-val))
+ #'(lambda (track)
+ (let ((track-val (emms-track-get track local-field)))
+ (and
+ track-val
+ (funcall local-operator local-compare-val track-val))))))
+@end lisp
+
+
+@node Factory Registration
+@section Factory Registration
+
+Registering a factory associates a name, a function and a list of prompt
+definitions so that we may create filters interactively by name. The
+prompting system will coerce the values given to the specified type
+providing select lists as indicated.
+
+The factory prompt data is used to interactively create new filters.
+A prompt is (prompt (type . select-list)) if there is no
+select list we read the value and coerce the value to the
+type as needed.
+
+These are the known coercion types.
+
+@itemize @bullet
+@item :number
+@item :string
+@item :list
+@item :symbol
+@item :function
+@end itemize
+
+Here is the Genre Factory which is actually made from the field-compare
+factory. This is a common pattern to create a simpler factory from a
+more complex one. It is simply a partial that is registered directly
+with a different set of prompts. In this case 'Genre:' is the prompt
+and it is expected to be a string.
+
+@lisp
+(emms-filters-register-filter-factory
+ "Genre"
+ (apply-partially 'emms-filters-make-filter-field-compare
+ 'string-equal-ignore-case 'info-genre)
+ '(("Genre: " (:string . nil))));;
+@end lisp
+
+The registration for the compare field factory is more complex because of
+the prompting for all the parameters. By changing just the registration
+name and the prompts we can create two factories, one for numbers and
+one for strings. Note the use of the ` and , to force the select lists
+to resolve within the lambda.
+
+Here is the registration for the number field compare factory. The
+operator function has a select list of number comparison functions. The
+field name has a select list of known numeric field names and the value
+to compare must be a number and will be coerced as needed.
+
+@lisp
+(emms-filters-register-filter-factory "Number field compare"
+ 'emms-filters-make-filter-field-compare
+ ;; prompts
+ `(("Compare Function: "
+ (:function . ,emms-filters-number-compare-functions))
+ ("Field name: "
+ (:symbol . ,emms-filters-number-field-names))
+ ("Compare to: "
+ (:number . nil))))
+@end lisp
+
+
+@node Defined Factories
+@section Defined Factories
+
+There are a number of defined factories derived from just a few functions.
+Most common filters can be easily made with these.
+There are a few predifined filters, but that has been kept to a minimum
+as filters can be a very personal thing. There are already filters for every
+track type and there many common genres and year range filters by decade.
+
+Filter factories like artist, album artist, composer, Names, etc.
+are all just specialized field compare or the fields search factories.
+
+Filter factories include the following.
+
+@itemize @bullet
+@item Album
+@item Album-artist
+@item All text fields
+@item Artist
+@item Artists
+@item Artists and composer
+@item Composer
+@item Directory
+@item Duration less
+@item Duration more
+@item Fields search
+@item Genre
+@item Greater than Year
+@item Less than Year
+@item Multi-filter
+@item Names
+@item Names and titles
+@item Not played since
+@item Notes
+@item Number field compare
+@item Orchestra
+@item Performer
+@item Played since
+@item String field compare
+@item Title
+@item Titles
+@item Track type
+@item Year range
+@end itemize
+
+
+@node Defining Filters
+@section Defining Filters
+
+Making a filter in elisp from a factory is easy.
+
+(emms-filters-make-filter <Factory Name> <Filter Name> <Factory Parameters>)
+
+The Genre Factory takes one string argument.
+@lisp
+(emms-filters-make-filter "Genre" "My Genre filter" "Somevalue")
+@end lisp
+
+Make a lot of filters at once with emms-filters-make-filters.
+
+@lisp
+(emms-filters-make-filters '(("Genre" "Waltz" "waltz")
+ ("Genre" "Salsa" "salsa")
+ ("Genre" "Blues" "blues")
+ ("Genre" "Jazz" "jazz")))
+@end lisp
+
+Filters can be easily created interactivly.
+Just push a filter onto the stack with @command{emms-filters-push},
+@command{emms-filters-and}, @command{emms-filters-or}, @command{emms-filters-and-not},
+or @command{emms-filters-squash},
+select 'new filter' then your factory and follow the prompts.
+
+Filters are added by name to their respective factory's filter ring.
+Here are some more complex filter definitions including some
+Multi-filter definitions, or meta-filters which are simply lists
+of filters by name, they are functionally equivalent to what
+is being built by the filter stack.
+
+@lisp
+(setq tango-filters
+ '(("Year range" "1900-1929" 1900 1929)
+ ("Year range" "1929-1937" 1929 1937)
+
+ ("Directory" "tangotunes" "tangotunesflac")
+
+ ("Genre" "Vals" "vals")
+ ("Genre" "Tango" "tango")
+ ("Genre" "Milonga" "milonga")
+
+ ("Multi-filter"
+ "1900-1937"
+ (("1900-1929" "1929-1937")))
+
+ ("Multi-filter"
+ "Vals | milonga"
+ (("Vals" "Milonga")))
+
+ ("Multi-filter"
+ "Vals 1900-1929"
+ (("Vals") ("1900-1929")))
+
+ ("Multi-filter"
+ "Not vals"
+ ((:not "Vals")))
+
+ ("Multi-filter"
+ "Vals or milonga 1900-1937"
+ (("Vals" "Milonga")
+ ("1900-1929" "1929-1937")))
+ ))
+
+(emms-filters-make-filters tango-filters)
+@end lisp
+
+A new entry in the Factory ring along with it's filters
+can also be easily added. This function deconstructs the definitions
+to facilitate the ease of addition. It can also be made from a
+simple list of names as well. The filters will appear both under their
+respective factories, and under this new menu item 'Tango'.
+They are not recreated, but simply listed by their names to be chosen.
+
+@lisp
+(emms-filters-add-filter-menu-from-filter-list "Tango" tango-filters)
+@end lisp
+
+Here is the easiest way to make the filter ring as used by the Browser.
+It is just a list of filter names.
+
+@lisp
+(emms-filters-make-filter-ring '("Tango" "Vals" "Milonga"))
+@end lisp
+
+The filter menu is automatically constructed as a ring of factory names
+as 'folders' that have a ring of filters. This filter menu tree can be
+added to in various ways. 'Keeping' a filter on the filter stack will
+temporarily add the multi-filter defined by the filter stack to the
+multi-filter ring.
+
+There are other ways to add to the filter menu tree.
+@command{emms-filters-add-to-filter-menu-from-filter-list} is used to deconstruct
+a variable holding filter defintions as in the example above in order
+to create a new ring in the menu tree.
+
+In turn that function uses @command{emms-filters-add-to-filter-menu} which takes
+a folder name and a filter or list of filters to place in the ring.
+The function @command{emms-filters-add-name-to-filter-menu} will add a filter by
+name to an existing filter folder/factory.
+
+It is also possible to view the filter menu tree as a message with
+@command{emms-filters-show-filter-menu}
+
+
+@node The Filter Stack
+@section The Filter Stack
+
+The filter stack builds more complex filters as you push filters to
+it. Adding to the filter or replacing it with another push creates a new
+meta-filter and it's multi-filter function to the filter stack. To return
+to the previous filter simply pop the stack. Each change to the stack, creates
+a meta-filter and it's corresponding constructed meta-filter. Any change
+results in a new 'current' multi-filter. The filters are represesented
+as are constructed names of the filters that created it.
+
+The filter stack uses meta-filters in a cons
+like this; (name . meta-filter).
+Filter names for meta-filters can be easily constructed from the filters
+they are made from. They aren't short but they work well enough.
+
+To use a filter, @command{emms-filters-push} it to create a new current filter
+on the stack. It will become a meta-filter on the filter stack and the
+current active filter will be a multi-filter version of it. The functions
+required to construct the current multi-filter are resolved at this time
+in a new multi-filter lambda function.
+
+The filter ring works independently of the filter stack. Each re-filtering
+of tracks uses the current ring filter and the current filter together.
+
+A filter on the stack can be 'kept'. The function @command{emms-filters-keep}
+will create and register a multi-filter of the current filter, adding
+it to the multi-filter menu. This only lasts until the current Emacs
+session ends. If @var{emms-filters-multi-filter-save-file} is set, keep will
+append a usable filter definition to the file for reuse as you wish.
+
+Other commands for manipulating the stack are listed here. Most
+should be self explanatory, Squash clears the stack, leaving the
+topmost filter. Smash is a clear followed by a push.
+
+@itemize @bullet
+@item @command{emms-filters-pop}
+@item @command{emms-filters-squash}
+@item @command{emms-filters-smash}
+@item @command{emms-filters-clear}
+@item @command{emms-filters-swap}
+@item @command{emms-filters-swap-pop}
+@item @command{emms-filters-keep}
+@end itemize
+
+An initial filter can be created with
+@command{emms-filters-push} or @command{emms-filters-smash} which is a clear followed by a push.
+
+Adding to the filter stack is done with
+@command{emms-filters-and}, @command{emms-filters-or}, @command{emms-filters-and-not},
+
+@node The Cache Stack
+@section The Cache Stack
+
+The cache stack is a simply a stack of emms-cache-db style hash tables.
+The full emms-cache-db is at the base of the stack and is always there.
+Each entry in the stack is a subset of the cache below it as a result
+of filtering. The stack entry names are constructed from the filters
+which created them.
+
+Filtering and displaying of tracks is done against the top cache on the stack.
+
+The function, @command{emms-filters-hard-filter} is the most common way to create
+an entry on the cache stack. It creates a cache from the current filter
+and cache, and pushes it to the stack. This does render the current filter
+as non-effective, so it can be cleared, or continue to grow depending
+on your desires. It can be useful to just keep going so that returning
+to the previous state is possible.
+
+One of the driving forces with creating cache entries was the way that
+the Emms-browser has always done searching. To this end, additional
+functionality was created to better emulate the browser's way of doing
+things. However the cache stack provides a lot of flexibility and power
+in how you navigate and search your music. Simply being able to repeatedly
+search and narrow the data is quite powerful all by itself.
+
+One-Shot filtering allows behavioral backward compatibility with the
+browser. One shots were created to emulate the browser's behavior of
+creating a subset cache from search-by. One shots push a filter, save
+to the cache stack and pop the filter, leaving only the cache.
+
+Using @command{emms-filters-one-shot} will push a filter, push a cache,
+then pop the filter. It will interactively prompt for a factory, the
+filter, and then the filter parameters to create a filter if none is
+given. @command{emms-filters-quick-one-shot} takes a factory name, and invokes
+the interactive creation of a new filter with that factory directly.
+The command @command{emms-filters-fields-search-quick-one-shot} is a one-shot
+using the fields-search filter factory, while adding to the fields-search
+ring in the filter menu-tree. The fields-search factory is the filter
+system's way of emulating browser's search-by functionality.
+
+These functions effectively allow the emulation of the browser's search
+behavior of quickly prompting, filtering and pushing a cache followed
+by a pop of the filter used. By the grace of that, simple wrapper
+functions for each of the browser's search functions were created
+using emms-filters-quick-one-shot. These functions are named after their browser
+equivalents as emms-filters-search-by-<field-names>. The browser search functions
+now call these filter system functions directly.
+
+Manipulating the cache stack is similar to manipulating the filter stack,
+The usual stack commands are:
+@itemize @bullet
+@item @command{emms-filters-pop-cache}
+@item @command{emms-filters-squash-caches}
+@item @command{emms-filters-clear-caches}
+@item @command{emms-filters-swap-cache}
+@item @command{emms-filters-swap-pop-cache}
+@item @command{emms-filters-push-cache}
+@item @command{emms-filters-stash-cache}
+@item @command{emms-filters-stash-pop-cache}
+@end itemize
+
+The functions @command{emms-filters-push-cache}, @command{emms-filters-stash-cache} and
+@command{emms-filters-stash-pop-cache} allow for a cache to be stashed and then
+later pushed back to the stack. The current cache on the stack can be
+stashed at anytime. The stashed caches will be a selection ring
+for @command{emms-filters-push-cache}.
+
+
+@node Showing State
+@section Showing State
+
+There are various functions that enable a view of all that is going
+on within the filter system. At the top level these are simply emacs
+messages which can be easily dismissed. Just below them, are equivalent
+functions that give formatted string versions for use as you like.
+
+The registered filter factories can be shown with
+@command{emms-filters-show-filter-factories}, the registered
+filters can be shown with @command{emms-filters-show-filters}.
+The @command{emms-filters-show-filter-menu} will show the current filter menu tree.
+
+The current filter ring can be shown with @command{emms-filters-show-filter-ring}
+and the filter stack can be shown with @command{emms-filters-current-meta-filter}.
+
+In code, the current filter name can be obtained with the
+@command{emms-filters-current-meta-filter-name}.
+The current ring filter name can be obtained with
+@command{emms-filters-current-ring-filter-name}
+
+Showing the cache stack is done with @command{emms-filters-show-cache-stack}.
+Any stashed caches can be seen with @command{emms-filters-show-cache-stash}
+which will also appear in the menu invoked by @command{emms-filters-push-cache}.
+
+Finally for a more complete report of the system use @command{emms-filters-status-print}
+which is a message of the formatted string given by @command{emms-filters-status}.
+
+In turn, the @command{emms-filters-status} is simply a format of the following four
+functions that give formatted strings of the moving parts of the filter system.
+
+@itemize @bullet
+@item @command{emms-filters-current-ring-filter}
+@item @command{emms-filters-current-meta-filter}
+@item @command{emms-filters-format-stack}
+@item @command{emms-filters-format-cache-stack}
+@end itemize
+
@c -------------------------------------------------------------------
@node Sorting Playlists
@@ -2935,8 +3865,8 @@ value. So instead of pressing @kbd{C-c +} six times to increase volume
by six steps of @code{emms-volume-change-amount}, you would simply type
@kbd{C-c + + + + + +}.
-Emms can change volume with amixer, mpd, mpv, PulseAudio and mixerctl
-out of the box, see @var{emms-volume-change-function}.
+Emms can change volume with amixer, mpd, PulseAudio and mixerctl out
+of the box, see @var{emms-volume-change-function}.
@c -------------------------------------------------------------------
diff --git a/emms-browser.el b/emms-browser.el
index 4bb2782..56a3991 100644
--- a/emms-browser.el
+++ b/emms-browser.el
@@ -55,6 +55,10 @@
;; (require 'emms-browser)
+;; Searching and filtering.
+;; -------------------------------------------------------------------
+;; See Emms-filters
+
;; Displaying covers
;; -------------------------------------------------------------------
@@ -82,56 +86,6 @@
;; You can download an example 'no cover' image from:
;; http://repose.cx/cover_small.jpg
-;; Filtering tracks
-;; -------------------------------------------------------------------
-
-;; If you want to display a subset of your collection (such as a
-;; directory of 80s music, only avi files, etc), then you can make
-;; some filters using code like this:
-
-;; ;; show everything
-;; (emms-browser-make-filter "all" 'ignore)
-
-;; ;; Set "all" as the default filter
-;; (emms-browser-set-filter (assoc "all" emms-browser-filters))
-
-;; ;; show all files (no streamlists, etc)
-;; (emms-browser-make-filter
-;; "all-files" (emms-browser-filter-only-type 'file))
-
-;; ;; show only tracks in one folder
-;; (emms-browser-make-filter
-;; "80s" (emms-browser-filter-only-dir "~/Mp3s/80s"))
-
-;; ;; show all tracks played in the last month
-;; (emms-browser-make-filter
-;; "last-month" (emms-browser-filter-only-recent 30))
-
-;; After executing the above commands, you can use M-x
-;; emms-browser-show-all, emms-browser-show-80s, etc to toggle
-;; between different collections. Alternatively you can use '<' and
-;; '>' to cycle through the available filters.
-
-;; The second argument to make-filter is a function which returns t if
-;; a single track should be filtered. You can write your own filter
-;; functions to check the type of a file, etc.
-
-;; Some more examples:
-
-;; ;; show only tracks not played in the last year
-;; (emms-browser-make-filter "not-played"
-;; (lambda (track)
-;; (not (funcall (emms-browser-filter-only-recent 365) track))))
-
-;; ;; show all files that are not in the pending directory
-;; (emms-browser-make-filter
-;; "all"
-;; (lambda (track)
-;; (or
-;; (funcall (emms-browser-filter-only-type 'file) track)
-;; (not (funcall
-;; (emms-browser-filter-only-dir "~/Media/pending") track)))))
-
;; Changing tree structure
;; -------------------------------------------------------------------
@@ -148,11 +102,11 @@
;; type)))
;; (defun toggle-album-display ()
-;; (if (string= emms-browser-current-filter-name "singles")
+;; (if (string= emms-filters-current-filter-name "singles")
;; (ad-activate 'emms-browser-next-mapping-type)
;; (ad-deactivate 'emms-browser-next-mapping-type)))
-;; (add-hook 'emms-browser-filter-changed-hook 'toggle-album-display)
+;; (add-hook 'emms-filters-filter-changed-hook 'toggle-album-display)
;; Changing display format
;; -------------------------------------------------------------------
@@ -182,8 +136,10 @@
;; %y the album year
;; %A the album name
;; %a the artist name of the track
+;; %o the album artist
;; %C the composer name of the track
;; %p the performer name of the track
+;; %g the genre of the track.
;; %t the title of the track
;; %T the track number
;; %cS a small album cover
@@ -235,7 +191,6 @@
;;; Code:
-(require 'cl-lib)
(require 'emms)
(require 'emms-cache)
(require 'emms-volume)
@@ -243,6 +198,8 @@
(require 'emms-playlist-sort)
(require 'sort)
(require 'seq)
+(require 'emms-filters)
+(require 'emms-cache)
;; --------------------------------------------------
@@ -351,6 +308,7 @@ Use nil for no sorting."
"Given a track, return t if the track should be ignored."
:type 'hook)
+;; Deprecated. See emms-filters-filter-changed-hook.
(defcustom emms-browser-filter-changed-hook nil
"Hook run after the filter has changed."
:type 'hook)
@@ -366,10 +324,6 @@ Called once for each directory."
(defvar emms-browser-buffer-name "*EMMS Browser*"
"The default buffer name.")
-
-(defvar emms-browser-search-buffer-name "*emms-browser-search*"
- "The search buffer name.")
-
(defvar emms-browser-top-level-hash nil
"The current mapping db, eg. artist -> track.")
(make-variable-buffer-local 'emms-browser-top-level-hash)
@@ -381,11 +335,52 @@ Called once for each directory."
(defvar emms-browser-current-indent nil
"Used to override the current indent, for the playlist, etc.")
-(defvar emms-browser-current-filter-name nil
- "The name of the current filter in place, if any.")
+;; Set the hooks for Emms-filters to say when to re-render.
+;; this is just a variable to mirror the browser's hook.
+;; It should probably just be set directly, and the browser's
+;; hook be deprecated. It will have to be set if anyone changes it...
+;; Potential problem if someone us using this hook.
+(add-hook 'emms-browser-filter-tracks-hook 'emms-filters-browser-filter-hook-function)
+(add-hook 'emms-filters-make-and-render-hash-hook 'emms-browse-by)
+(add-hook 'emms-filters-expand-render-hook 'emms-browser-expand-all)
+
+
+(defvar emms-browser-tree-node-map-default
+ '((info-albumartist . info-artist)
+ (info-artist . info-album)
+ (info-composer . info-album)
+ (info-performer . info-album)
+ (info-album . info-title)
+ (info-genre . info-artist)
+ (info-year . info-artist))
+ "How to build the browse tree, by album artist, artist, album.")
+
+(defvar emms-browser-tree-node-map-AAgAt
+ '((info-albumartist . info-genre)
+ (info-artist . info-title)
+ (info-composer . info-album)
+ (info-performer . info-album)
+ (info-album . info-albumartist)
+ (info-genre . info-artist)
+ (info-year . info-album))
+ "How to build the browse tree, by album artist, genre, artist")
+
+(defvar emms-browser-tree-node-map-AAAgt
+ '((info-albumartist . info-artist)
+ (info-artist . info-genre)
+ (info-composer . info-album)
+ (info-performer . info-album)
+ (info-album . info-albumartist)
+ (info-genre . info-title)
+ (info-year . info-album))
+ "How to build the browse tree, by album artist, artist, genre")
+
+(defvar emms-browser-tree-node-map emms-browser-tree-node-map-default
+ "The alist mapping of the browser tree node map.")
(defvar emms-browser-mode-map
(let ((map (make-sparse-keymap)))
+ (define-key map (kbd "Q") #'emms-filters-pop-cache)
(define-key map (kbd "q") #'emms-browser-bury-buffer)
(define-key map (kbd "/") #'emms-isearch-buffer)
(define-key map (kbd "r") #'emms-browser-goto-random)
@@ -414,27 +409,63 @@ Called once for each directory."
(define-key map (kbd "b 4") #'emms-browse-by-year)
(define-key map (kbd "b 5") #'emms-browse-by-composer)
(define-key map (kbd "b 6") #'emms-browse-by-performer)
- (define-key map (kbd "s a") #'emms-browser-search-by-artist)
- (define-key map (kbd "s c") #'emms-browser-search-by-composer)
- (define-key map (kbd "s p") #'emms-browser-search-by-performer)
- (define-key map (kbd "s A") #'emms-browser-search-by-album)
- (define-key map (kbd "s t") #'emms-browser-search-by-title)
- (define-key map (kbd "s s") #'emms-browser-search-by-names)
+ (define-key map (kbd "b 7") #'emms-browse-by-albumartist)
+ (define-key map (kbd "W o w") #'emms-browser-lookup-albumartist-on-wikipedia)
(define-key map (kbd "W A w") #'emms-browser-lookup-artist-on-wikipedia)
(define-key map (kbd "W C w") #'emms-browser-lookup-composer-on-wikipedia)
(define-key map (kbd "W P w") #'emms-browser-lookup-performer-on-wikipedia)
(define-key map (kbd "W a w") #'emms-browser-lookup-album-on-wikipedia)
- (define-key map (kbd ">") #'emms-browser-next-filter)
- (define-key map (kbd "<") #'emms-browser-previous-filter)
(define-key map (kbd "+") #'emms-volume-raise)
(define-key map (kbd "-") #'emms-volume-lower)
- map)
- "Keymap for `emms-browser-mode'.")
-(defvar emms-browser-search-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map emms-browser-mode-map)
- (define-key map (kbd "q") #'emms-browser-kill-search)
+ (define-key map (kbd ">") #'emms-filters-next-ring-filter)
+ (define-key map (kbd "<") #'emms-filters-previous-ring-filter)
+ (define-key map (kbd "f !") #'emms-filters-clear-ring-filter)
+ (define-key map (kbd "f >") #'emms-filters-next-ring-filter)
+ (define-key map (kbd "f <") #'emms-filters-previous-ring-filter)
+
+ (define-key map (kbd "i s") #'emms-filters-status-print)
+ (define-key map (kbd "i f") #'emms-filters-show-filters)
+ (define-key map (kbd "i m") #'emms-filters-show-filter-menu)
+ (define-key map (kbd "i F") #'emms-filters-show-filter-factories)
+ (define-key map (kbd "i r") #'emms-filters-show-filter-ring)
+ (define-key map (kbd "i c") #'emms-filters-show-cache-stack)
+ (define-key map (kbd "i S") #'emms-filters-show-cache-stash)
+
+ (define-key map (kbd "f q") #'emms-filters-pop)
+ (define-key map (kbd "f h") #'emms-filters-hard-filter)
+ (define-key map (kbd "f r") #'emms-filters-swap) ; rotate ?
+ (define-key map (kbd "f R") #'emms-filters-swap-pop) ; rotate-eject, ,pop-previous
+ (define-key map (kbd "f f") #'emms-filters-squash) ;flatten
+ (define-key map (kbd "f k") #'emms-filters-keep)
+ (define-key map (kbd "f C") #'emms-filters-clear-all)
+ (define-key map (kbd "f c") #'emms-filters-clear)
+ (define-key map (kbd "f p") #'emms-filters-push)
+ (define-key map (kbd "f s") #'emms-filters-smash)
+ (define-key map (kbd "f o") #'emms-filters-or)
+ (define-key map (kbd "f a") #'emms-filters-and)
+ (define-key map (kbd "f n") #'emms-filters-and-not)
+
+ (define-key map (kbd "c p") #'emms-filters-push-cache)
+ (define-key map (kbd "c z") #'emms-filters-stash-pop-cache)
+ (define-key map (kbd "c Z") #'emms-filters-stash-cache)
+ (define-key map (kbd "c q") #'emms-filters-pop-cache)
+ (define-key map (kbd "c h") #'emms-filters-hard-filter)
+ (define-key map (kbd "c r") #'emms-filters-swap-cache)
+ (define-key map (kbd "c R") #'emms-filters-swap-pop-cache)
+ (define-key map (kbd "c S") #'emms-filters-squash-caches)
+ (define-key map (kbd "c c") #'emms-filters-clear-caches)
+
+ (define-key map (kbd "s o") #'emms-filters-search-by-albumartist)
+ (define-key map (kbd "s a") #'emms-filters-search-by-artist)
+ (define-key map (kbd "s c") #'emms-filters-search-by-composer)
+ (define-key map (kbd "s p") #'emms-filters-search-by-performer)
+ (define-key map (kbd "s A") #'emms-filters-search-by-album)
+ (define-key map (kbd "s t") #'emms-filters-search-by-title)
+ (define-key map (kbd "s T") #'emms-filters-search-by-titles)
+ (define-key map (kbd "s n") #'emms-filters-search-by-names)
+ (define-key map (kbd "s s") #'emms-filters-search-by-names-and-titles)
+ (define-key map (kbd "s e") #'emms-filters-search-by-all-text) ;everything.
map)
"Keymap for `emms-browser-mode'.")
@@ -495,14 +526,17 @@ example function is `emms-browse-by-artist'."
(defun emms-browser-mode (&optional no-update)
"A major mode for the Emms browser.
+Does not set the browser buffer to current unless NO-UPDATE is set.
\\{emms-browser-mode-map}"
;; create a new buffer
(interactive)
+ (kill-all-local-variables)
- (use-local-map emms-browser-mode-map)
(setq major-mode 'emms-browser-mode
mode-name "Emms-Browser")
+ (use-local-map emms-browser-mode-map)
+
(setq buffer-read-only t)
(unless no-update
(setq emms-browser-buffer (current-buffer)))
@@ -525,14 +559,13 @@ example function is `emms-browse-by-artist'."
(emms-browser-create))))
(defun emms-browser-get-buffer ()
- "Return the current buffer if it exists, or nil.
-If a browser search exists, return it."
- (or (get-buffer emms-browser-search-buffer-name)
- (unless (or (null emms-browser-buffer)
- (not (buffer-live-p emms-browser-buffer)))
- emms-browser-buffer)))
+ "Return the current buffer if it exists, or nil."
+ (unless (or (null emms-browser-buffer)
+ (not (buffer-live-p emms-browser-buffer)))
+ emms-browser-buffer))
(defun emms-browser-ensure-browser-buffer ()
+ "Ensure the current buffer is the browser buffer."
(unless (eq major-mode 'emms-browser-mode)
(error "Current buffer is not an emms-browser buffer")))
@@ -551,7 +584,7 @@ If a browser search exists, return it."
;; subelements will be stored in a bdata alist structure.
(defmacro emms-browser-add-category (name type)
- "Create an interactive function emms-browse-by-NAME."
+ "Create an interactive function with NAME and info TYPE emms-browse-by-NAME."
(let ((funname (intern (concat "emms-browse-by-" name)))
(funcdesc (concat "Browse by " name ".")))
`(defun ,funname ()
@@ -559,24 +592,35 @@ If a browser search exists, return it."
(interactive)
(emms-browse-by ,type))))
-(defun emms-browse-by (type)
- "Render a top level buffer based on TYPE."
+(defun emms-browse-by (&optional type)
+ "Render a top level buffer based on TYPE.
+If TYPE is not given default to top-level-type
+or the default-browse-type"
+ (if (not type)
+ (setq type (or emms-browser-top-level-type
+ emms-browser-default-browse-type)))
;; FIXME: assumes we only browse by info-*
(let* ((name (substring (symbol-name type) 5))
(modedesc (concat "Browsing by: " name))
- (hash (emms-browser-make-hash-by type)))
- (when emms-browser-current-filter-name
+ (hash (emms-browser-make-hash-by type))
+ (current-filter-name (emms-filters-full-name)))
+ (when current-filter-name
(setq modedesc (concat modedesc
- " [" emms-browser-current-filter-name "]")))
+ " [" current-filter-name "]")))
(emms-browser-clear)
(rename-buffer modedesc)
(emms-browser-render-hash hash type)
(setq emms-browser-top-level-hash hash)
(setq emms-browser-top-level-type type)
- (unless (> (hash-table-count hash) 0)
- (emms-browser-show-empty-cache-message))
- (goto-char (point-min))))
+ (goto-char (point-min))
+
+ (if (not (> (hash-table-count hash) 0))
+ (if (emms-filters-is-filtering)
+ (emms-browser-show-empty-result-message)
+ (emms-browser-show-empty-cache-message)))))
+
+(emms-browser-add-category "albumartist" 'info-albumartist)
(emms-browser-add-category "artist" 'info-artist)
(emms-browser-add-category "composer" 'info-composer)
(emms-browser-add-category "performer" 'info-performer)
@@ -636,8 +680,8 @@ For \\='info-year TYPE, use \\='info-originalyear, \\='info-originaldate and
:test emms-browser-comparison-test))
field existing-entry)
(maphash (lambda (_path track)
- (unless (run-hook-with-args-until-success
- 'emms-browser-filter-tracks-hook track)
+ (when (run-hook-with-args-until-success
+ 'emms-browser-filter-tracks-hook track)
(setq field
(emms-browser-get-track-field track type))
(when field
@@ -645,11 +689,11 @@ For \\='info-year TYPE, use \\='info-originalyear, \\='info-originaldate and
(if existing-entry
(puthash field (cons track existing-entry) hash)
(puthash field (list track) hash)))))
- emms-cache-db)
+ (emms-filters-last-search-cache))
hash))
(defun emms-browser-render-hash (db type)
- "Render a mapping (DB) into a browser buffer."
+ "Render a mapping (DB) with TYPE into a browser buffer."
(maphash (lambda (desc data)
(emms-browser-insert-top-level-entry desc data type))
db)
@@ -682,6 +726,11 @@ For \\='info-year TYPE, use \\='info-originalyear, \\='info-originaldate and
(let ((bdata (emms-browser-make-bdata-tree type 1 tracks name)))
(emms-browser-insert-format bdata)))
+(defun emms-browser-show-empty-result-message ()
+ "Display some help if the cache-db exists but the result hash is empty."
+ (emms-with-inhibit-read-only-t
+ (insert (emms-filters-empty-result-message))))
+
(defun emms-browser-show-empty-cache-message ()
"Display some help if the cache is empty."
(emms-with-inhibit-read-only-t
@@ -709,18 +758,11 @@ browser, and hit 'b 1' to refresh.")))
;; --------------------------------------------------
;; Building a subitem tree
;; --------------------------------------------------
-
(defun emms-browser-next-mapping-type (current-mapping)
"Return the next sensible mapping.
Eg. if CURRENT-MAPPING is currently \\='info-artist, return
\\='info-album."
- (cond
- ((eq current-mapping 'info-artist) 'info-album)
- ((eq current-mapping 'info-composer) 'info-album)
- ((eq current-mapping 'info-performer) 'info-album)
- ((eq current-mapping 'info-album) 'info-title)
- ((eq current-mapping 'info-genre) 'info-artist)
- ((eq current-mapping 'info-year) 'info-artist)))
+ (alist-get current-mapping emms-browser-tree-node-map))
(defun emms-browser-make-bdata-tree (type level tracks name)
"Build a tree of browser DB elements for tracks."
@@ -731,7 +773,7 @@ Eg. if CURRENT-MAPPING is currently \\='info-artist, return
type level))
(defun emms-browser-make-bdata-tree-recurse (type level tracks)
- "Build a tree of alists based on a list of tracks, TRACKS.
+ "Build a tree of alists based on TYPE, LEVEL and a list of tracks, TRACKS.
For example, if TYPE is \\='info-year, return an alist like:
artist1 -> album1 -> *track* 1.."
(let* ((next-type (emms-browser-next-mapping-type type))
@@ -755,18 +797,25 @@ artist1 -> album1 -> *track* 1.."
alist))))
(defun emms-browser-make-name (entry type)
- "Return a name for ENTRY, used for making a bdata object."
- (let ((key (car entry))
- (track (cadr entry))
- artist title) ;; only the first track
- (cond
- ((eq type 'info-title)
- (setq artist (emms-track-get track 'info-artist))
- (setq title (emms-track-get track 'info-title))
- (if (not (and artist title))
- key
- (concat artist " - " title)))
- (t key))))
+ "Return a name for ENTRY and TYPE, used for making a bdata object."
+
+ (if (eq type 'info-title)
+ (let* ((track (cadr entry))
+ (artist (emms-track-get track 'info-artist))
+ (aartist (emms-track-get track 'info-albumartist))
+ (title (emms-track-get track 'info-title))
+
+ (artist (if (and artist aartist)
+ (concat aartist " : " artist)
+ (if (and (not artist) aartist)
+ artist
+ artist))))
+
+ ;; return a title or the car of entry
+ (if (and artist title)
+ (concat artist " - " title)
+ (car entry)))
+ (car entry)))
(defun emms-browser-track-number (track)
"Return a string representation of a track number.
@@ -781,7 +830,7 @@ return an empty string."
tracknum)))))
(defun emms-browser-disc-number (track)
- "Return a string representation of a track number.
+ "Return a string representation of the TRACK number.
The string will end in a space. If no track number is available,
return an empty string."
(let ((discnum (emms-track-get track 'info-discnumber)))
@@ -790,7 +839,7 @@ return an empty string."
discnum)))
(defun emms-browser-year-number (track)
- "Return a string representation of a track\\='s year.
+ "Return a string representation of a TRACK\\='s year.
This will be in the form \\='(1998) \\='."
(let ((year (emms-track-get-year track)))
(if (or (not (stringp year)) (string= year "0"))
@@ -799,7 +848,7 @@ This will be in the form \\='(1998) \\='."
"(" year ") "))))
(defun emms-browser-track-duration (track)
- "Return a string representation of a track duration.
+ "Return a string representation of the TRACK duration.
If no duration is available, return an empty string."
(let ((pmin (emms-track-get track 'info-playing-time-min))
(psec (emms-track-get track 'info-playing-time-sec))
@@ -908,11 +957,12 @@ Uses `emms-browser-alpha-sort-function'."
alist))
(defun emms-browser-sort-by-year-or-name (alist)
- "Sort based on year or name."
+ "Sort ALIST based on year or name."
(sort alist (emms-browser-sort-cadr
'emms-browser-sort-by-year-or-name-p)))
(defun emms-browser-sort-by-year-or-name-p (a b)
+ "Sort A and B by on year or name."
;; FIXME: this is a bit of a hack
(let ((a-desc (concat
(emms-browser-year-number a)
@@ -927,6 +977,7 @@ Uses `emms-browser-alpha-sort-function'."
(let ((sort-func
(cond
((or
+ (eq type 'info-albumartist)
(eq type 'info-artist)
(eq type 'info-composer)
(eq type 'info-performer)
@@ -937,9 +988,10 @@ Uses `emms-browser-alpha-sort-function'."
emms-browser-album-sort-function)
((eq type 'info-title)
'emms-browser-sort-by-track)
- (t (message "Can't sort unknown mapping!")))))
+ (t (message (concat "Can't sort unknown mapping!" type))))))
(funcall sort-func alist)))
+
;; --------------------------------------------------
;; Subitem operations on the buffer
;; --------------------------------------------------
@@ -1088,21 +1140,21 @@ Stops at the next line at the same level, or EOF."
;; --------------------------------------------------
(defun emms-browser-playlist-insert-group (bdata)
- "Insert a group description into the playlist buffer."
+ "Insert a group description of BDATA into the playlist buffer."
(let ((name (emms-browser-format-line bdata 'playlist)))
(with-current-emms-playlist
(goto-char (point-max))
(insert name "\n"))))
(defun emms-browser-playlist-insert-track (bdata)
- "Insert a track into the playlist buffer."
+ "Insert a track from BDATA into the playlist buffer."
(let ((name (emms-browser-format-line bdata 'playlist)))
(with-current-emms-playlist
(goto-char (point-max))
(insert name "\n"))))
(defun emms-browser-playlist-insert-bdata (bdata starting-level)
- "Add all tracks in BDATA to the playlist."
+ "Add all tracks in BDATA at STARTING-LEVEL to the playlist."
(let ((type (emms-browser-bdata-type bdata))
(level (emms-browser-bdata-level bdata))
emms-browser-current-indent)
@@ -1281,7 +1333,7 @@ Return the playlist buffer point-max before adding."
(emms-browser-show-subitems))))
(defun emms-browser-next-non-track (&optional direction)
- "Jump to the next non-track element."
+ "Jump to the next non-track element in DIRECTION."
(interactive)
(let ((continue t))
(while (and continue
@@ -1296,9 +1348,13 @@ Return the playlist buffer point-max before adding."
(emms-browser-next-non-track -1))
(defun emms-browser-expand-all ()
- "Expand everything."
+ "Expand everything.
+This function is used by Emms-filters as the expand-render-hook, it must
+must be certain that there is a bdata tree to expand."
(interactive)
- (emms-browser-expand-to-level 99))
+ (when (emms-browser-level-at-point)
+ (emms-browser-mark-and-collapse)
+ (emms-browser-expand-to-level 99)))
(defun emms-browser-expand-to-level-2 ()
"Expand all top level items one level."
@@ -1339,7 +1395,7 @@ Return the playlist buffer point-max before adding."
(emms-browser-subitems-visible))))
(defun emms-browser-view-in-dired (&optional bdata)
- "View the current directory in dired."
+ "View the current directory from BDATA or bdata at point in DIRED."
;; FIXME: currently just grabs the directory from the first track
(interactive)
(if bdata
@@ -1353,11 +1409,12 @@ Return the playlist buffer point-max before adding."
(defun emms-browser-remove-tracks (&optional delete start end)
"Remove all tracks at point or in region if active.
-Unless DELETE is non-nil or with prefix argument, this only acts on the browser,
-files are untouched.
-If caching is enabled, files are removed from the cache as well.
-When the region is not active, a numeric prefix argument remove that many
-tracks from point, it does not delete files."
+Unless DELETE is non-nil or with prefix argument, this only acts on the
+browser,files are untouched. Optionally with line number at position
+START and position of END within the region. If caching is enabled,
+files are removed from the cache as well. When the region is not active,
+a numeric prefix argument remove that many tracks from point, it does
+not delete files."
(interactive "P\nr")
(let ((count (cond
((use-region-p)
@@ -1406,6 +1463,7 @@ tracks from point, it does not delete files."
(put 'emms-browser-delete-files 'disabled t)
(defun emms-browser-clear-playlist ()
+ "Clear playlist."
(interactive)
(with-current-emms-playlist
(emms-playlist-clear)))
@@ -1420,9 +1478,14 @@ tracks from point, it does not delete files."
(concat url data)))))
(defun emms-browser-lookup-wikipedia (field)
+ "Lookup contents of FIELD in wikipedia."
(emms-browser-lookup
field "http://en.wikipedia.org/wiki/Special:Search?search="))
+(defun emms-browser-lookup-albumartist-on-wikipedia ()
+ (interactive)
+ (emms-browser-lookup-wikipedia 'info-albumartist))
+
(defun emms-browser-lookup-artist-on-wikipedia ()
(interactive)
(emms-browser-lookup-wikipedia 'info-artist))
@@ -1537,10 +1600,10 @@ Returns the playlist window."
;; make q in the playlist window hide the linked browser
(when (boundp 'emms-playlist-mode-map)
(define-key emms-playlist-mode-map (kbd "q")
- (lambda ()
- (interactive)
- (emms-browser-hide-linked-window)
- (bury-buffer))))
+ (lambda ()
+ (interactive)
+ (emms-browser-hide-linked-window)
+ (bury-buffer))))
(setq pwin (get-buffer-window pbuf))))
pwin))
@@ -1556,96 +1619,6 @@ Returns the playlist window."
;; linked buffer
(bury-buffer other-buf)))
-;; --------------------------------------------------
-;; Searching
-;; --------------------------------------------------
-
-(defun emms-browser-filter-cache (search-list)
- "Return a list of tracks that match SEARCH-LIST.
-SEARCH-LIST is a list of cons pairs, in the form:
-
- ((field1 field2) string)
-
-If string matches any of the fields in a cons pair, it will be
-included."
-
- (let (tracks)
- (maphash (lambda (_k track)
- (when (emms-browser-matches-p track search-list)
- (push track tracks)))
- emms-cache-db)
- tracks))
-
-(defun emms-browser-matches-p (track search-list)
- (let (no-match matched)
- (dolist (item search-list)
- (setq matched nil)
- (dolist (field (car item))
- (let ((track-field (emms-track-get track field "")))
- (when (and track-field (string-match (cadr item) track-field))
- (setq matched t))))
- (unless matched
- (setq no-match t)))
- (not no-match)))
-
-(defun emms-browser-search-buffer-go ()
- "Create a new search buffer, or clean the existing one."
- (switch-to-buffer
- (get-buffer-create emms-browser-search-buffer-name))
- (emms-browser-mode t)
- (use-local-map emms-browser-search-mode-map)
- (emms-with-inhibit-read-only-t
- (delete-region (point-min) (point-max))))
-
-(defun emms-browser-search (fields)
- "Search for STR using FIELDS."
- (let* ((prompt (format "Searching with %S: " fields))
- (str (read-string prompt)))
- (emms-browser-search-buffer-go)
- (emms-with-inhibit-read-only-t
- (emms-browser-render-search
- (emms-browser-filter-cache
- (list (list fields str)))))
- (emms-browser-expand-all)
- (goto-char (point-min))))
-
-(defun emms-browser-render-search (tracks)
- (let ((entries
- (emms-browser-make-sorted-alist 'info-artist tracks)))
- (dolist (entry entries)
- (emms-browser-insert-top-level-entry (car entry)
- (cdr entry)
- 'info-artist))))
-
-;; hmm - should we be doing this?
-(defun emms-browser-kill-search ()
- "Kill the buffer when q is hit."
- (interactive)
- (kill-buffer (current-buffer)))
-
-(defun emms-browser-search-by-artist ()
- (interactive)
- (emms-browser-search '(info-artist)))
-
-(defun emms-browser-search-by-composer ()
- (interactive)
- (emms-browser-search '(info-composer)))
-
-(defun emms-browser-search-by-performer ()
- (interactive)
- (emms-browser-search '(info-performer)))
-
-(defun emms-browser-search-by-title ()
- (interactive)
- (emms-browser-search '(info-title)))
-
-(defun emms-browser-search-by-album ()
- (interactive)
- (emms-browser-search '(info-album)))
-
-(defun emms-browser-search-by-names ()
- (interactive)
- (emms-browser-search '(info-artist info-composer info-performer info-title info-album)))
;; --------------------------------------------------
;; Album covers
@@ -1776,18 +1749,21 @@ If > album level, most of the track data will not make sense."
("y" . ,(emms-track-get-year track))
("A" . ,(emms-track-get track 'info-album))
("a" . ,(emms-track-get track 'info-artist))
+ ("o" . ,(emms-track-get track 'info-albumartist))
("C" . ,(emms-track-get track 'info-composer))
("p" . ,(emms-track-get track 'info-performer))
("t" . ,(emms-track-get track 'info-title))
+ ("g" . ,(emms-track-get track 'info-genre))
("D" . ,(emms-browser-disc-number track))
("T" . ,(emms-browser-track-number track))
("d" . ,(emms-browser-track-duration track))))
str)
(when (equal type 'info-album)
- (setq format-choices (append format-choices
- `(("cS" . ,(emms-browser-get-cover-str path 'small))
- ("cM" . ,(emms-browser-get-cover-str path 'medium))
- ("cL" . ,(emms-browser-get-cover-str path 'large))))))
+ (setq format-choices
+ (append format-choices
+ `(("cS" . ,(emms-browser-get-cover-str path 'small))
+ ("cM" . ,(emms-browser-get-cover-str path 'medium))
+ ("cL" . ,(emms-browser-get-cover-str path 'large))))))
(when (functionp format)
@@ -1832,6 +1808,7 @@ If > album level, most of the track data will not make sense."
(name (cond
((or (eq type 'info-year)
(eq type 'info-genre)) "year/genre")
+ ((eq type 'info-albumartist) "albumartist")
((eq type 'info-artist) "artist")
((eq type 'info-composer) "composer")
((eq type 'info-performer) "performer")
@@ -1887,7 +1864,7 @@ the text that it generates."
;; function for specifiers which may be empty.
(defvar emms-browser-default-format "%i%n"
- "indent + name")
+ "Indent + name.")
;; tracks
(defvar emms-browser-info-title-format
@@ -1964,79 +1941,92 @@ the text that it generates."
name
" in a browser/playlist buffer."))))
-(emms-browser-make-face "year/genre" "#aaaaff" "#444477" 1.5)
-(emms-browser-make-face "artist" "#aaaaff" "#444477" 1.3)
-(emms-browser-make-face "composer" "#aaaaff" "#444477" 1.3)
-(emms-browser-make-face "performer" "#aaaaff" "#444477" 1.3)
-(emms-browser-make-face "album" "#aaaaff" "#444477" 1.1)
-(emms-browser-make-face "track" "#aaaaff" "#444477" 1.0)
+(emms-browser-make-face "albumartist" "#aaaabb" "#444455" 1.3)
+(emms-browser-make-face "year/genre" "#aaaaff" "#444477" 1.5)
+(emms-browser-make-face "artist" "#aaaaff" "#444477" 1.3)
+(emms-browser-make-face "composer" "#aaaaff" "#444477" 1.3)
+(emms-browser-make-face "performer" "#aaaaff" "#444477" 1.3)
+(emms-browser-make-face "album" "#aaaaff" "#444477" 1.1)
+(emms-browser-make-face "track" "#aaaaff" "#444477" 1.0)
;; --------------------------------------------------
;; Filtering
;; --------------------------------------------------
+;;; Deprecated Browser filter making.
+;;; Filters made in this way will continue working
+;;; with emms-filters.
+;;; Working directly with emms-filters is better.
+;;; See emms-filters.el.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; I dont know if anyone uses this function, but here is one
+;; just in case. Probably a bad idea.
+(defun emms-browser-refilter (filter)
+ "Push FILTER to emms-filter-stack and re-render.
+Filter can be a string name or a filter cons.
+Non-destructive if a filter of the same filter name already
+exists.
+
+Deprecated. See emms-filters.
+
+Equivalent to `(emms-filters-push filter-name)' when using a registered
+emf filter directly.
+
+The FILTER will be registered with emms-filters if it is
+a cons filter and its name is not already taken.
+
+The filter name will be pushed to the emms-filters-filter-stack, making
+it the active filter."
+ (when (not (stringp filter))
+ (emms-filters-register-if-missing filter))
+ (emms-filters-push
+ (if (stringp filter)
+ filter
+ (car filter))))
-(defvar emms-browser-filters nil
- "A list of available filters.")
+(defmacro emms-browser-make-filter (name filter-func)
+ "Make a user-level function for filtering tracks and put
+it into the emms-filters-filter-ring.
+
+Deprecated: See emms-filters-make-filter
+
+Altered from the original to invert the return value of the filter.
+The resulting inverted filter is used directly in emms-filters like any
+other emf filter.
-(defmacro emms-browser-make-filter (name func)
- "Make a user-level function for filtering tracks.
This:
- - defines an interactive function M-x emms-browser-show-NAME.
- - defines a variable emms-browser-filter-NAME of (name . func).
- - adds the filter to `emms-browser-filters'."
+ - Defines a filter cons variable emms-browser-filter-NAME of (name . func).
+ - The filter is registered into emms-filters-filters.
+ - Added to the emf filter menus under `browser-filters'.
+ - Added to the emms-filters-filter-ring.
+ - Defines an interactive function emms-browser-show-NAME."
(let ((funcnam (intern (concat "emms-browser-show-" name)))
(var (intern (concat "emms-browser-filter-" name)))
(desc (concat "Filter the cache using rule '"
name "'")))
`(progn
(defvar ,var nil ,desc)
- (setq ,var (cons ,name ,func))
- (add-to-list 'emms-browser-filters ,var)
+ (setq ,var (cons ,name
+ (lambda (track) ;; invert the func.
+ (not (funcall ,filter-func track)))))
+ ;;(add-to-list 'emms-browser-filters ,var)
+ (emms-filters-register-filter-into-ring ,var)
(defun ,funcnam ()
,desc
(interactive)
- (emms-browser-refilter ,var)))))
-
-(defun emms-browser-set-filter (filter)
- "Set the current filter to be used on next update.
-This does not refresh the current buffer."
- (setq emms-browser-filter-tracks-hook (cdr filter))
- (setq emms-browser-current-filter-name (car filter))
- (run-hooks 'emms-browser-filter-changed-hook))
-
-(defun emms-browser-refilter (filter)
- "Filter and render the top-level tracks."
- (emms-browser-set-filter filter)
- (emms-browse-by (or emms-browser-top-level-type
- emms-browser-default-browse-type)))
-
-(defun emms-browser-next-filter (&optional reverse)
- "Redisplay with the next filter."
- (interactive)
- (let* ((list (if reverse
- (reverse emms-browser-filters)
- emms-browser-filters))
- (key emms-browser-current-filter-name)
- (next (cadr (member (assoc key list) list))))
- ;; wrapped
- (unless next
- (setq next (car list)))
- (emms-browser-refilter next)))
-
-(defun emms-browser-previous-filter ()
- "Redisplay with the previous filter."
- (interactive)
- (emms-browser-next-filter t))
+ (emms-filters-push name)))))
+;; The original, inverted, emms-browser filters examples
+;; Works with the above make-filter macro and Emms-filters.
+;; Deprecated.
(defun emms-browser-filter-only-dir (dirname)
- "Generate a function which checks if a track is in DIRNAME.
+ "Generate a function to check if a track is in DIRNAME.
If the track is not in DIRNAME, return t."
(let ((re (concat "^" (expand-file-name dirname))))
(lambda (track)
(not (string-match re (emms-track-get track 'name))))))
(defun emms-browser-filter-only-type (type)
- "Generate a function which checks a track's type.
+ "Generate a function to check a track's type.
If the track is not of TYPE, return t."
(lambda (track)
(not (eq type (emms-track-get track 'type)))))
@@ -2053,7 +2043,42 @@ If the track is not of TYPE, return t."
(emms-track-get track 'last-played nil))
(time-less-p min-date last-played))))))
+;; --------------------------------------------------
+;; Searching
+;; --------------------------------------------------
+;; These functions are here for backward compatibility.
+;; See emms-filters.el.
+
+(defun emms-browser-search-by-albumartist ()
+ (interactive)
+ (emms-filters-search-by-albumartist))
+
+(defun emms-browser-search-by-artist ()
+ (interactive)
+ (emms-filters-search-by-artist))
+
+(defun emms-browser-search-by-composer ()
+ (interactive)
+ (emms-filters-search-by-composer))
+
+(defun emms-browser-search-by-performer ()
+ (interactive)
+ (emms-filters-search-by-performer))
+
+(defun emms-browser-search-by-title ()
+ (interactive)
+ (emms-filters-search-by-title))
+
+(defun emms-browser-search-by-album ()
+ (interactive)
+ (emms-filters-search-by-album))
+
+(defun emms-browser-search-by-names ()
+ (interactive)
+ (emms-filters-search-by-names-and-titles))
+
+;;; Thumbnails
;; TODO: Add function to clear the cache from thumbnails that have no associated
;; cover folders. This is especially useful in case the music library path
;; changes: currently, all covers will have to be re-cached while the old ones
@@ -2165,7 +2190,7 @@ will always use the same cover per folder.
;; TODO: Add image resizing support to Emacs.
(setq msg (with-output-to-string
(with-current-buffer standard-output
- (setq err (call-process emms-browser-thumbnail-convert-program nil '(t t) nil
+ (setq err (call-process (executable-find "convert") nil '(t t) nil
"-resize" (format "%sx%s" size-value size-value)
cover
cache-dest-file)))))
diff --git a/emms-filters.el b/emms-filters.el
new file mode 100644
index 0000000..2eaff8a
--- /dev/null
+++ b/emms-filters.el
@@ -0,0 +1,2119 @@
+;;; emms-filters.el --- Filters for Emms -*- lexical-binding: t; -*-
+;; Copyright (C) 2023 2024 2025
+;; Author: Erica Lina Qi <EricaLinaQi@proton.me>
+;; Keywords: emms, filter, search, cache, stack
+
+;;; Commentary:
+;; This code allows you to filter and search the metadata cache.
+;; This manages the search and filter functionalities of emms-browser.
+
+;; Usage
+;; -------------------------------------------------------------------
+;; Use filters as before with <> keys to cycle the filter ring in the browser buffer.
+;; Search-by something to create a new cache, or emms-filters-push to get started
+;; building a filter.
+;;
+;; Use 'emms-filters-status-print' to watch the stacks and filters in effect.
+
+;; Use 'emms-filters-show-filter-menu' to see a list of all filters known
+;; organized by factory.
+;;
+;; Apply a filter with the functions
+;; emms-filters-push, emms-filters-or, emms-filters-and, emms-filters-and-not
+;; emms-filters-smash and emms-filters-one-shot.
+;;
+;; manipulate the stack with the functions:
+;; emms-filters-push, emms-filters-pop, emms-filters-clear and emms-filters-squash, swap and swap-pop.
+;;
+;; Interactively create and use new filters by choosing 'new filter'
+;; in the filter selection lists.
+;;
+;; The function `emms-filters-current-meta-filter' gives the multi-filter data source
+;; for the the current filter.
+;;
+;; A filter can be 'kept'. The function 'emms-filters-keep will create and register
+;; a multi-filter of the current filter, adding it to the multi-filter menu.
+;; This only lasts until the current Emacs session ends.
+;; If emms-filters-multi-filter-save-file is set, a usable multi-filter definition will also be
+;; appended to the file.
+;;
+;; Manage the search cache with emms-filters-hard-filter, emms-filters-one-shot, emms-filters-quick-one-shot,
+;; emms-filters-search-by, emms-filters-pop-cache, emms-filters-squash-caches,
+;; emms-filters-clear-caches, emms-filters-push-cache
+;;
+;; Caches can be stashed for the session and pushed back to the stack
+;; at any time. The Emms-cache-DB is the default.
+;;
+;; Switch the active ring filter with <> which correspond to
+;; emms-filters-next-ring-filter and emms-filters-previous-ring-filter.
+;;
+;; The filter stack can be cleared with emms-filters-clear, the caches
+;; with emms-filters-clear-caches and the ring with emms-filters-clear-ring-filter.
+;;
+;; All stacks and filters can be cleared with 'emms-filters-clear-all
+
+
+;; Some Definitions:
+;; -------------------------------------------------------------------
+;; Filtering: Displaying the narrowed results from looking for matches
+;; in a list of items.
+;; Search:
+;; The saving of the narrowed results created from filtering a list of items,
+;; such that future filtering and searching will have a smaller list of items.
+;;
+;; Filter or filter cons, a cons of the form (name . function)
+;; Registration takes care of this.
+;; Once a filter is properly constructed it will be a cons
+;; (name . filter-function) the functions are created with one of the
+;; filter factories.
+;;
+;; Filter function - a function that takes a track as its argument
+;; and returns true or False.
+;;
+;; Filter Factory: A function which creates a filter function given the
+;; the desired parameters.
+;;
+;; Multi-filter: A filter factory which is other filters combined
+;; using Or, And as well as And-Not.
+;;
+;; Meta-filter: A multi-filter data definition.
+;; The filter stack uses meta-filters in a cons
+;; like this; (name . meta-filter).
+;; Filter names for meta-filters can be easily constructed.
+;;
+;; This meta-filter uses 4 filters by name:
+;;
+;; '(("Vals" "Milonga")
+;; ("1900-1929" "1929-1937"))
+;;
+;; This filter will
+;; Match on genre of vals OR
+;; milonga AND
+;; any year between
+;; 1900-1929 OR
+;; 1929-1937.
+;;
+;; Making one or more multi-filter is easy.
+;; (emms-filters-make-filters
+;; '(("Multi-filter"
+;; "Vals | milonga - 1900-1937"
+;; (("Vals" "Milonga")
+;; ("1900-1929" "1929-1937")))));
+
+;; Meta-filter-stack: An interactive stack of meta-filters which allow
+;; the creation, combination and use of all filters.
+;;
+;; Filter-ring: A ring of filter names, which can be easily selected with
+;; next and previous controls. All filters created through
+;; 'emms-browser-make-filter are added here by default.
+;;
+;; The filter ring replaces the functionality of emms-browser-filters.
+;; The easiest way to make the filter ring is with a list of filters.
+;; (emms-filters-make-filter-ring '("Tango" "Vals" "Milonga"))
+
+
+;; Backward compatibility:
+;; -------------------------------------------------------------------
+;; This code replaces both emms-browser filters and search-by.
+;; emms-browser-make-filter and search-by use emms-filters for their
+;; current functionality.
+;;
+;; Emms-browser-filter functions are specified to return an inverted value.
+;; emms-browser-make-filter is a slightly different mechanism from emms-filters.el.
+;; but has been modified to pass its filters to emms-filters.
+;; Those filters will be properly inverted and added to emms-filters-filters and to the
+;; emms-filters-filter-ring. This should provide a seamless experience for previous users
+;; of emms-browser filtering. As the emms-filters-filter-ring is functionally equivalent.
+;;
+;; Search-by was just one filter factory, 'fields-search', and searches are
+;; not inverted. The only real difference between a filter and a search was
+;; that a filter was rendered and a search was saved for subsequent filtering.
+;; The equivalent to the emms-browser search-by is just a one shot
+;; interactive new fields-search factory filter that saves a cache.
+;;
+;; Filters are slightly different when coded for emms-filters.
+;; 1. They should return true if they match the tracks
+;; 2. The factory should wrap the lambda in a let with lexical-binding t.
+;; 3. The factory and the filters must both be registered with emms-filters.
+;; This provides a higher level of interaction with the filters.
+;; 4. There is no difference between a search function and a filter function.
+
+
+;; The moving parts.
+;; -------------------------------------------------------------------
+;; Emms-filters consists of a few different mechanisms.
+;; There are factories to make filters. There is the filter stack
+;; to manage the creation and use of filters.
+;;
+;; There is the cache stack to handle the saving of a current filtered results
+;; into a reduced database cache for subsequent searches.
+;;
+;; There is the filter ring for quickly switching between commonly used filters.
+;;
+;; - Filter Factories - To make filters, which are lambda functions.
+;; Factories are frequently made from other factories.
+;; - Filters - To be used by the meta-filter stack to create more filters.
+;; filters are represeed simply as data, and are very easy to define.
+;; - Filter menu - A customizable ring of factories and their rings of filters.
+;; - Multi-filter - A filter factory to create Meta-filters, filters made of filters.
+;; - Meta-filter - A multi-filter data definition. Also data, and easy to define.
+;; - The filter stack - A meta-filter manipulator and multi-filter creator.
+;; - The cache stack - A stack of database caches.
+;; - The filter ring. - A subset of convenient to use filters.
+;; For backward compatibility and convenience.
+
+
+;;; Filter factories
+;; -------------------------------------------------------------------
+;; Filter factories make filters which are simply test functions which
+;; take a track and return true or false.
+;;
+;; Factories are registered with the Emms-filter system so that they
+;; have names that can be referenced later. Additionally, registration
+;; includes a prompt and parameter definition. This allows the emms-filters
+;; prompting system to provide an interactive interface to any filter factory.
+;;
+;; The prompting system allows the creation of any filter interactively at
+;; any time.
+;;
+;; Here is the Genre Factory which is actually made from the
+;; field-compare factory. This is a common pattern to create
+;; a simpler factory from a more complex one. It is simply
+;; a partial that is registered with a different set of prompts.
+;; In this case Genre: is the prompt and it is expected to be a string.
+;;
+;; (emms-filters-register-filter-factory
+;; "Genre"
+;; (apply-partially 'emms-filters-make-filter-field-compare
+;; 'string-equal-ignore-case 'info-genre)
+;; '(("Genre: " (:string . nil))));;
+;;
+;; The actual filter factory is the field comparison factory.
+;; This single function can be a new factory for any data field
+;; using any comparison function we would like.
+;;
+;; Filter factories depend upon lexical context of their parameters. In
+;; order to have data values that stick after function creation there
+;; is let using lexical binding to ensure the factory behaves as expected.
+;; This transfers the values to local values and uses them as local
+;; within the returned #'(lambda (track)...).
+;;
+;; (defun emms-filters-make-filter-field-compare (operator-func field compare-val)
+;; "Make a filter that compares FIELD to COMPARE-VALUE with OPERATOR-FUNC.
+;; Works for number fields and string fields provided the appropriate
+;; type match between values and the comparison function. Partials can
+;; easily make more specific factory functions from this one."
+;; (let ((local-operator operator-func)
+;; (local-field field)
+;; (local-compare-val compare-val))
+;; #'(lambda (track)
+;; (let ((track-val (emms-track-get track local-field)))
+;; (and
+;; track-val
+;; (funcall local-operator local-compare-val track-val))))))
+
+;; The registration for this factory is more complex because of the prompting
+;; for all the parameters. By changing just the registration name and the
+;; prompts we can create two factories, one for numbers and one for strings.
+;; Note the use of the ` and , to force the select lists to resolve.
+;;
+;; (emms-filters-register-filter-factory "Number field compare"
+;; 'emms-filters-make-filter-field-compare
+;; ;; prompts
+;; `(("Compare Function: "
+;; (:function . ,emms-filters-number-compare-functions))
+;; ("Field name: "
+;; (:symbol . ,emms-filters-number-field-names))
+;; ("Compare to: "
+;; (:number . nil))))
+;;
+;;
+;; Making a filter from a factory is easy.
+;;
+;; (emms-filters-make-filter "Genre" "My Genre filter" "Somevalue")
+;;
+;; Or make a lot of filters at once.
+;;
+;; (emms-filters-make-filters '(("Genre" "Waltz" "waltz")
+;; ("Genre" "Salsa" "salsa")
+;; ("Genre" "Blues" "blues")
+;; ("Genre" "Jazz" "jazz")))
+;;
+;; Or just push a filter onto the stack with emms-filters-push,
+;; select 'new filter' and follow the prompts.
+
+;;; Factory Prompts.
+;;; Interactive factory prompting for filter building.
+;; -------------------------------------------------------------------
+;; Registering a factory associates a name, a function and a list of
+;; prompt definitions so that we may create filters interactively by name.
+;;
+;; The factory prompt data is used to interactively create new filters.
+;; A prompt is (prompt (type . select-list)) if there is no
+;; select list we read a string and coerce the value to the correct
+;; type as needed. :number, :string, :list :symbol :function
+;; are the coercion type choices.
+;;
+;; Here is a simple factory registration for the Genre filter factory function.
+;; Which takes a single string parameter.
+;;
+;; (emms-filters-register-filter-factory "Genre"
+;; emms-filters-make-filter-genre
+;; '(("Genre: " (:string . nil))))
+;;
+;; Parameters are of the form: '((prompt (type . select-list)) ... )
+;;
+;; The following prompt will coerce the value it receives into a number.
+;;
+;; '(("Days: " (:number . nil)))
+;;
+;; The compare field factory takes a compare function,
+;; an :info-field specifier and a string to compare.
+;; Note the use of ` and , in order to resolve the selection lists here.
+;; It uses the convenience variables which hold the compare functions and
+;; string field names.
+;;
+;; `(("Compare Function:"
+;; (:function . ,emms-filters-string-compare-functions))
+;; ("Field name:"
+;; (:symbol . ,emms-filters-string-field-names))
+;; ("Compare to:"
+;; (:string . nil)));;
+;;
+
+;; The Filter stack
+;; -------------------------------------------------------------------
+;; The filter stack builds more complex filters as you push filters to it.
+;; Adding to the filter or replacing it with another push continues to add filters
+;; to the filter stack. To return to the previous filter simply pop the stack.
+;;
+;; To use a filter, emms-filters-push it to create a new current filter on the stack.
+;; It will become a meta-filter on the filter stack
+;; and the current active filter will be a multi-filter version of it.
+;;
+;; The filter ring works independently of the filter stack. Each re-filtering of
+;; tracks uses the current ring filter and the current filter together.
+;;
+;; A filter can be 'kept'. The function 'emms-filters-keep will create and register
+;; a multi-filter of the current filter, adding it to the multi-filter menu.
+;; This only lasts until the current Emacs session ends.
+;; If emms-filters-multi-filter-save-file is set, a usable filter definition will be
+;; appended to the file.
+;;
+;; Other commands for manipulating the stack.
+;; Push, pop, squash, clear, swap, swap-pop, smash
+
+
+;;; The Search Cache Stack
+;; --------------------------------------------------
+;; The cache stack is a simply a stack of emms-cache-db style hash tables.
+;; Each entry is a subset of the master emms-cache-db created through filtering.
+;; Their names are constructed from the filters which created them.
+;;
+;; Filtering and displaying of tracks is done against the top cache on the stack.
+;;
+;; The function; emms-filters-hard-filter creates a cache from the current filter
+;; and cache, and pushes it to the stack.
+;;
+;; By using emms-filters-one-shot, emms-filters-quick-one-shot
+;; also create caches on the stack. These functions allow effective
+;; emulation of the previous EMMS-Browser search functionalities.
+;;
+;; The usual commands exist for manipulating the stack.
+;; Pop, squash, clear, swap, swap-pop, push-cache
+
+;; Additionally there is a stash option. This pops and stashes the current
+;; cache to be retrieved later. The stashed cache will become a selection
+;; for the push-cache command.
+;;
+;; A one-shot filter combined with a factory name is 'emms-filters-quick-one-shot.
+;;
+;; This effectively emulates the former emms-browser search behavior of
+;; quickly prompting, filtering and saving a cache by pushing a filter,
+;; hard-filter then pop.
+
+
+;; How it works.
+;; -------------------------------------------------------------------
+;; To begin simply do an emms-filters-push. This will present the filter factory ring.
+;; Choose a factory, an already existing filter or 'New' and follow the prompts.
+;;
+;; Filters which are created interactively can be kept for the session
+;; with emms-filters-keep. One shots, (searches), are automatically kept for the session.
+;; Keep may also write them to a file for later use.
+;;
+;; Push a filter to the filter stack with emms-filters-push and then
+;; add to it with the emms-filters-or, emms-filters-and, and emms-filters-and-not functions.
+;; Each change results in new filter on the stack.
+;;
+;; Use emms-filters-or to add another filter and choose 'new filter' to
+;; interactively create and add a filter to the current filter.
+;;
+;; Add in an extra layer of quick switch filtering with next and previous
+;; filter-ring filters. The filter ring filters can be accessed with
+;; < and >.
+;;
+;; You may want to keep your results for a while, or you may
+;; wish to start with a clear search for a name, either way,
+;; a hard-filter will push a cache-db onto the cache stack.
+;;
+;; Subsequent filtering continues with this new DB cache. A cache can also
+;; be pushed to the stack with a one-shot function. One shots
+;; make, use, cache, and then pop a filter, leaving a new cache and the filter
+;; stack as it was.
+;;
+;; Create a new factory function, register it in
+;; emms-filters-factories along with its parameters and prompts.
+;; From this point on filters can be created interactively by selecting
+;; to push a new filter, and choosing the new factory.
+;;
+;; In code use emms-filters-make-filter or emms-filters-make-filters to use the factory by name.
+
+;; Filter stack interaction
+;; -------------------------------------------------------------------
+;; To interactively create a filter, start with a push.
+;; The filter stack itself is the interactive filter factory for multi-filters.
+;;
+;; Choose a factory, then an existing filter or 'new filter' and follow the prompts.
+;;
+;; Do an emms-filters-or to add another possible match or emms-filters-and or
+;; emms-filters-and-not to add a restriction. Build the filter how you like.
+;;
+;; When a new filter is pushed, it turns into a meta filter
+;; and is pushed on the filter stack. A filter function is made from
+;; the entire stack's multi-filter and set to be the current filter,
+;; and the browser is asked to re-render the results.
+;;
+;; Any change to the stack causes a re-render with the new current filter.
+;;
+;; Use emms-filters-status or the emms-filter-hydra to see the stacks and
+;; current filters.
+
+;; Making Filters from factories, in code.
+;; -------------------------------------------------------------------
+;; Filter factories include the following. Most common filters can be
+;; easily constructed from these. The number of available filters is too
+;; numerous to list. For instance, a filter already exists for every
+;; track type and there many common genres and year range filters.
+;;
+;; Filter factories like artist, album artist, composer, Names, etc.
+;; are all just specialized field compare or fields search factories.
+;;
+;; Factories
+;; ----------
+;; Album
+;; Album-artist
+;; All text fields
+;; Artist
+;; Artists
+;; Artists and composer
+;; Composer
+;; Directory
+;; Duration less
+;; Duration more
+;; Fields search
+;; Genre
+;; Greater than Year
+;; Less than Year
+;; Multi-filter
+;; Names
+;; Names and titles
+;; Not played since
+;; Notes
+;; Number field compare
+;; Orchestra
+;; Performer
+;; Played since
+;; String field compare
+;; Title
+;; Titles
+;; Track type
+;; Year range
+
+;; Filters also have names, and are added
+;; to their respective factory's filter selection menu.
+;; here are some example filter definitions.
+;;
+;; ;; Filters are easily described as data.
+;; ;; factory Name arguments
+;;
+;; (setq tango-filters
+;; '(("Year range" "1900-1929" 1900 1929)
+;; ("Year range" "1929-1937" 1929 1937)
+;; ("Directory" "tangotunes" "tangotunesflac")
+;;
+;; ("Genre" "Vals" "vals")
+;; ("Genre" "Tango" "tango")
+;; ("Genre" "Milonga" "milonga")
+;;
+;; ("Multi-filter"
+;; "1900-1937"
+;; (("1900-1929" "1929-1937")))
+;;
+;; ("Multi-filter"
+;; "Vals | milonga"
+;; (("Vals" "Milonga")))
+;;
+;; ("Multi-filter"
+;; "Vals 1900-1929"
+;; (("Vals") ("1900-1929")))
+;;
+;; ("Multi-filter"
+;; "Not vals"
+;; ((:not "Vals")))
+;;
+;; ("Multi-filter"
+;; "Vals or milonga 1900-1937"
+;; (("Vals" "Milonga")
+;; ("1900-1929" "1929-1937")))
+;; ))
+;;
+;; (emms-filters-make-filters tango-filters)
+;;
+;; ;; Add my own filter selection menu with tango filters in it.
+;; (emms-filters-add-filter-menu-from-filter-list "Tango" tango-filters)
+;;
+;; The easiest way to make a filter ring.
+;; (emms-filters-make-filter-ring '("Tango" "Vals" "Milonga"))
+
+;;; Code:
+
+(require 'emms-cache)
+(require 'ring)
+(require 'cl-lib)
+
+(defvar emms-filters-stack nil
+ "A history of multi-filters. Our working stack.")
+
+(defvar emms-filters-search-caches '()
+ "The stack of search result caches.")
+
+(defvar emms-filters-filter-ring nil
+ "A ring of filter names for quick access with next and previous.")
+
+(defconst emms-filters-no-filter nil ;; '("no filter" . nil)
+ "A filter that turns filtering off, a better initial value than nil.")
+
+(defvar emms-filters-current-ring-filter emms-filters-no-filter
+ "The current ring filter, a filter cons, (name . func).")
+
+(defvar emms-filters-filter-factories '()
+ "An alist of filter factory functions and their argument lists.")
+
+(defvar emms-filters-filters '(("no filter" . nil))
+ "A list of available filters.")
+
+(defvar emms-filters-automatic-filter-names t
+ "Automatically generate filter names when creating filters interactively.")
+
+(defvar emms-filters-current-filter emms-filters-no-filter
+ "The current filter function.")
+
+(defvar emms-filters-current-filter-name "no filter"
+ "A name string of the filter for everyone to use.")
+
+(defvar emms-filters-filter-menu '("no filter" "new filter")
+ "A list of available filters grouped by factory.")
+
+(defgroup emms-filters nil
+ "*The Emacs Multimedia System filter system"
+ :prefix "emms-filters-"
+ :group 'multimedia
+ :group 'applications)
+
+;; For backwards compatibility with emms-browser
+;; This is really just a mirror of the browser's hook.
+(defcustom emms-filters-filter-changed-hook nil
+ "Hook to run after the filter has changed."
+ :type 'hook)
+
+;; Emms-filters is agnostic about the renderer.
+;;
+;; These are to be set by the rendererer so that emms-filters
+;; can ask for a new render of the results when
+;; new a new filter has been created.
+
+(defvar emms-filters-make-and-render-hash-hook nil
+ "This function applies the filters, creates a hash,
+and then populates and renders a tree of data,
+For the Emms-browser this should be emms-browse-by.")
+
+;; emms-filters-expand-render-hook
+(defvar emms-filters-expand-render-hook nil
+ "To be set by the renderer so that the results tree
+can be expanded when a filter or search exists,
+For the Emms-Browser this is the emms-browser-expand-all function.")
+
+(defvar emms-filters-multi-filter-save-file nil
+ "A file name to write the kept meta-filters from the session to.")
+
+(defvar emms-filters-cache-stash '(("Emms DB" . emms-cache-db))
+ "A list of cons (name . cache).")
+
+(defun emms-filters-browser-filter-hook-function (track)
+ "A hook function for the browser. Freewill here for TRACK filtering.
+First we test the track against the current ring filter if we have one,
+then we combine with the result of the emms-filters-current-filter."
+ (and (if (cdr emms-filters-current-ring-filter)
+ (funcall (cdr emms-filters-current-ring-filter) track)
+ t)
+ (if (cdr emms-filters-current-filter)
+ (funcall (cdr emms-filters-current-filter) track)
+ t)))
+
+(defun emms-filters-register-filter (filter-name filter)
+ "Put our new FILTER function named FILTER-NAME in our filter list."
+ (push (cons filter-name filter) emms-filters-filters))
+
+(defun emms-filters-register-if-missing (filter)
+ "Register a cons FILTER if it isn't already in the emms-filters-filters list."
+ (when (not (assoc (car filter) emms-filters-filters))
+ (push filter emms-filters-filters )))
+
+;; (defun emms-filters-add-filter-menu-item (folder-name name-list)
+;; "Add a list of NAME-LIST, a list of strings,
+;; as another FOLDER-NAME in the filter selection menu."
+;; (setq emms-filters-filter-menu
+;; (cons (list folder-name name-list)
+;; emms-filters-filter-menu)))
+
+(defun emms-filters-add-to-filter-menu-from-filter-list (folder filters)
+ "Add a FOLDER and FILTERS to the filter select list menu."
+ (emms-filters-add-to-filter-menu folder (mapcar 'cadr filters)))
+
+(defun emms-filters-add-to-filter-menu (folder-name filter-or-list)
+ "Add to a FOLDER-NAME in the filter select menu creating it as needed.
+Adds filter name(s) given in FILTER-OR-LIST to the FOLDER-NAME
+of the filter select menu tree."
+ (if (listp filter-or-list)
+ (mapcar (lambda (filter)
+ (emms-filters-add-name-to-filter-menu folder-name filter))
+ filter-or-list)
+ (emms-filters-add-name-to-filter-menu folder-name filter-or-list)))
+
+(defun emms-filters-add-name-to-filter-menu (folder-name filter-name)
+ "Add FILTER-NAME to menu tree of FOLDER-NAME."
+ (if (assoc folder-name emms-filters-filter-menu)
+ (push filter-name (cadr (assoc folder-name emms-filters-filter-menu)))
+ (setq emms-filters-filter-menu
+ (cons (list folder-name (list filter-name))
+ emms-filters-filter-menu))))
+
+(defun emms-filters-show-filter-menu ()
+ "Show the menu tree of filters."
+ (interactive)
+ (message "%s"
+ (mapconcat
+ (lambda (menu)
+ (if (consp menu)
+ (format "%s : \n%s\n"
+ (car menu)
+ (mapconcat 'identity
+ (cadr menu) ", "))
+ menu))
+ emms-filters-filter-menu "\n")))
+
+(defun emms-filters-make-filter-ring (list-of-filter-names)
+ "Make a ring of filter names from the LIST-OF-FILTER-NAMES.
+Appends the `no filter' filter."
+ (setq emms-filters-filter-ring
+ (make-ring
+ (+ 1 (length list-of-filter-names))))
+ (mapcar (lambda (filter-name)
+ (ring-insert emms-filters-filter-ring filter-name))
+ (cons "no filter" list-of-filter-names)))
+
+(defun emms-filters-append-to-filter-ring (filter-name)
+ "Append a single FILTER-NAME to the filter-ring.
+This creates the filter ring as needed."
+ (if emms-filters-filter-ring
+ (ring-insert+extend emms-filters-filter-ring
+ filter-name t)
+ (emms-filters-make-filter-ring (list filter-name))))
+
+;; This should allow people to continue using the emms-browser
+;; filtering as they always have, reusing the filters they've already made.
+(defun emms-filters-register-filter-into-ring (filter)
+ "Integrate Emms browser filters into emms-filters-filters.
+Register a FILTER to emms-filters-filters if it's name is missing.
+Add its name to the filter ring and filter menu in
+the `browser-filters' selection menu."
+ (emms-filters-register-if-missing filter)
+ (let ((name (car filter)))
+ (emms-filters-append-to-filter-ring name)
+ (emms-filters-add-to-filter-menu "browser-filters" name)))
+
+(defun emms-filters-list-filters ()
+ "List the filters in our filter list."
+ (mapcar 'car emms-filters-filters))
+
+(defun emms-filters-show-filters ()
+ "Show the filters we have."
+ (interactive)
+ (when emms-filters-filters
+ (message "Emf Filters:\n%s"
+ (mapconcat 'identity (emms-filters-list-filters) "\n"))))
+
+(defun emms-filters-show-filter-ring ()
+ "Show the filters in the filter ring."
+ (interactive)
+ (message "Ring filters: %s" (ring-elements emms-filters-filter-ring)))
+
+(defun emms-filters-find-filter (name)
+ "A nicer way to find NAME in our list of filters."
+ (assoc name emms-filters-filters))
+
+(defun emms-filters-find-filter-function (filter-name)
+ "Find the Function for FILTER-NAME in emms-filters-filters.
+Pass functions through untouched."
+ (if (eq filter-name :not)
+ :not
+ (cdr (assoc filter-name emms-filters-filters))))
+
+(defun emms-filters-format-search (fields value)
+ "Create a string format from a list of FIELDS and a compare VALUE."
+ (format "%s : %s"
+ (mapconcat
+ #'(lambda (info)
+ (if (symbolp info)
+ (substring (symbol-name info) 5)
+ info))
+ fields " | ")
+ value))
+
+;; The Filter Factory of factories.
+;; making them, using them, keeping them organized.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defun emms-filters-make--filter (factory factory-args)
+ "Make a filter using the FACTORY and FACTORY-ARGS.
+If factory is a function it is used directly. Otherwise, it will
+look for the function in emms-filters-filter-factories."
+ (let ((factory-func (if (functionp factory)
+ factory
+ (cadr (assoc factory emms-filters-filter-factories)))))
+ (apply factory-func factory-args)))
+
+(defun emms-filters-make-filter (factory filter-name factory-args)
+ "Make a filter named FILTER-NAME using the FACTORY and FACTORY-ARGS.
+If factory is a function it is used directly. Otherwise, it will
+look for the function in emms-filters-filter-factories."
+ (emms-filters-add-to-filter-menu factory filter-name)
+ (emms-filters-register-filter
+ filter-name
+ (emms-filters-make--filter factory factory-args)))
+
+(defun emms-filters-make-filters (filter-list)
+ "Make filters in FILTER-LIST into filter functions.
+The filter list holds entries specified as
+ (factory-name filter-name factory-arguments)."
+ (mapcar (lambda (filter)
+ (emms-filters-make-filter
+ (car filter)
+ (cadr filter) (cddr filter)))
+ filter-list))
+
+(defun emms-filters-new-filter (&optional factory-name make-filter-name)
+ "Build a new filter from a filter factory interactively.
+Use FACTORY-NAME instead of prompting if given.
+If MAKE-FILTER-NAME or EMMS-FILTERS-AUTOMATIC-FILTER-NAMES is true the name will
+be constructed instead of prompted.
+
+Normally prompts for a filter factory and its parameters, prompts for a
+filter name and then creates and registers a new filter,then returns its name."
+ (interactive)
+ (let* ((factory-name (if factory-name factory-name
+ (emms-filters-choose-factory)))
+ (make-name (or make-filter-name emms-filters-automatic-filter-names))
+ (parameters (emms-filters-get-factory-parameters factory-name))
+ (filter-name (if make-name
+ (format "%s : %s" factory-name parameters)
+ (read-string "filter name:"))))
+
+ (message "%s | %s parms %s" factory-name filter-name parameters)
+
+ (emms-filters-make-filter factory-name filter-name parameters)
+ filter-name))
+
+(defun emms-filters-register-filter-factory (name func prompt-list)
+ "Register FUNC as NAME with PROMPT-LIST into a filter choice.
+Give it the shape: (name . (func . prompt-list))."
+ (push
+ (cons name (cons func prompt-list))
+ emms-filters-filter-factories))
+
+(defun emms-filters-list-filter-factories ()
+ "List the filters in our factories list."
+ (mapcar 'car emms-filters-filter-factories))
+
+(defun emms-filters-show-filter-factories ()
+ "Show the filter factories we have."
+ (interactive)
+ (when emms-filters-filter-factories
+ (message "Filter Factories:\n%s"
+ (mapconcat #'identity (emms-filters-list-filter-factories) "\n "))))
+
+;; (message "Emms Cache stack:\n %s\n"
+;; (mapconcat #'identity (emms-filters-get-search-keys) "\n "))
+
+(defun emms-filters-clear-filter-factories ()
+ "Reset the filter factory list."
+ (setq emms-filters-filter-factories nil))
+
+
+;;; Factory Prompting.
+;;
+;; This function is a bit brittle for my taste.
+;; It needs more use cases.
+;; It might be the only one, or we only ever have lists of symbols...
+;; This is used for the fields-search factory.
+(defun emms-filters-string-field-list-prompt (prompt)
+ "Recursively PROMPT for elements of a list.
+Prompt must define a select list. The only usage example so
+far is the field-search list which is all symbols.
+ info-artist, info-genre, `intern-soft' works for those."
+ (let* ((prompt-string (car prompt))
+ (selections (cdar (cdr prompt)))
+ (value
+ (completing-read prompt-string
+ (cons "quit" selections) nil t)))
+ (if (string= value "quit")
+ nil
+ (cons (intern-soft value)
+ (emms-filters-string-field-list-prompt
+ (cons (concat (car prompt) " " value)
+ (cdr prompt)))))))
+
+(defun emms-filters-coerce-prompt-value (prompt value)
+ "Coerce VALUE, a string, according to the prompt type inside PROMPT.
+PROMPT should be in the form (prompt (type . <select-list>)).
+Types are :number, :symbol, :string and :function.
+Strings pass through."
+ (let ((type (car (cadr prompt))))
+ (cond
+ ((string= type :number) (string-to-number value))
+ ((string= type :symbol) (intern-soft value))
+ ((string= type :function) (intern-soft value))
+ (t value))))
+
+(defun emms-filters-read-string-or-choose (prompt)
+ "Choose the method input using PROMPT.
+Do a string read or completing read if PROMPT has a select-list.
+Do a recursive completing read with selection-list if a :list type.
+A prompt should look like this; (prompt (type . <select-list>))."
+ (let* ((prompt-string (car prompt))
+ (selections (cdr (cadr prompt)))
+ (_ (message "Selections %s" selections))
+ (type (car (cadr prompt)))
+ (value (cond ((string= type :list) (emms-filters-string-field-list-prompt prompt))
+ (selections
+ (completing-read prompt-string selections nil t))
+ (t (read-string prompt-string)))))
+ (emms-filters-coerce-prompt-value prompt value)))
+
+(defun emms-filters-get-factory-parameters (factory-name)
+ "Prompt for the parameters needed by a factory identified by FACTORY-NAME.
+Coerce their types as indicated and return the list of parameters.
+
+A prompt should be of the form (prompt (type . <list>)) where prompt is a string
+and type is :number :function :symbol or :string"
+ (interactive)
+ (let ((prompts (cddr (assoc factory-name emms-filters-filter-factories))))
+ (mapcar (lambda (prompt)
+ (emms-filters-read-string-or-choose prompt))
+ prompts)))
+
+
+;;; Factory Functions to make filter functions with.
+;; A filter factory is a function that returns a function which
+;; returns true if it likes the values from the track it was given.
+;;
+;; Registering them makes them interactive and invokable
+;; by name.
+
+(defun emms-filters-make-filter-directory (dirname)
+ "Generate a function to check if a track is in DIRNAME.
+If the track is not in DIRNAME, return t.
+Uses a regex anchoring dirname to the beginning of the expanded path."
+ (let ((re (concat "^" (expand-file-name dirname))))
+ #'(lambda (track)
+ (string-match re (emms-track-get track 'name)))))
+
+(emms-filters-register-filter-factory "Directory"
+ 'emms-filters-make-filter-directory
+ '(("Directory: " (:string . nil))))
+
+;; seconds in a day (* 60 60 24) = 86400
+(defun emms-filters-make-filter-played-within (days)
+ "Show only tracks played within the last number of DAYS."
+ (let ((seconds-to-time (seconds-to-time (* days 86400))))
+ #'(lambda (track)
+ (let ((min-date (time-subtract
+ (current-time)
+ seconds-to-time))
+ last-played)
+ (and (setq last-played
+ (emms-track-get track 'last-played nil))
+ (time-less-p min-date last-played))))))
+
+(emms-filters-register-filter-factory "Played since"
+ 'emms-filters-make-filter-played-within
+ '(("Days: " (:number . nil))))
+
+(defun emms-filters-make-filter-not-played-within (days)
+ "Make a not played since DAYS filter."
+ (lambda (track)
+ (funcall (emms-filters-make-filter-played-within days) track)))
+
+(emms-filters-register-filter-factory "Not played since"
+ 'emms-filters-make-filter-not-played-within
+ '(("Days: " (:number . nil))))
+
+;; Getting the year is special. It might be in year or date.
+(defun emms-filters-get-year (track)
+ "Get the year from a TRACK. Check year and date fields.
+Returns a number"
+ (let* ((year (emms-track-get track 'info-year))
+ (date (emms-track-get track 'info-date))
+ (year (or year (emms-format-date-to-year date)))
+ (year (and year (string-to-number year))))
+ year))
+
+(defun emms-filters-make-filter-year-range (y1 y2)
+ "Make a date range filter from Y1 and Y2."
+ (let ((local-y1 y1)
+ (local-y2 y2))
+ #'(lambda (track)
+ (let ((year (emms-filters-get-year track)))
+ (and
+ year
+ (<= local-y1 year)
+ (>= local-y2 year))))))
+
+(emms-filters-register-filter-factory "Year range"
+ 'emms-filters-make-filter-year-range
+ '(("Start year:" (:number . nil))
+ ("End year:" (:number . nil))))
+
+(defun emms-filters-make-filter-year-greater (year)
+ "Make a Greater than year filter from YEAR."
+ (let ((local-year year))
+ #'(lambda (track)
+ (let ((year (emms-filters-get-year track)))
+ (and
+ year
+ (<= local-year year))))))
+
+(emms-filters-register-filter-factory "Greater than Year"
+ 'emms-filters-make-filter-year-greater
+ '(("Greater than year: " (:number . nil))))
+
+(defun emms-filters-make-filter-year-less (year)
+ "Make a Less than year filter from YEAR."
+ (let ((local-year year))
+ #'(lambda (track)
+ (let ((year (emms-filters-get-year track)))
+ (and
+ year
+ (>= local-year year))))))
+
+(emms-filters-register-filter-factory "Less than Year"
+ 'emms-filters-make-filter-year-less
+ '(("Less than year: " (:number . nil))))
+
+;; fields-search
+;; -------------
+;; A replacement filter factory for the emms-browser-fields-search filter.
+(defun emms-filters-make-filter-fields-search (fields compare-value)
+ "Make a filter to search in a list of track FIELDS for COMPARE-VALUE.
+This replaces the original emms-browser search match-p functionality."
+ (let ((local-fields fields)
+ (local-compare-value compare-value))
+ #'(lambda (track)
+ (cl-reduce
+ (lambda (result field)
+ (let ((track-value (emms-track-get track field "")))
+ (or result
+ (and track-value
+ (string-match local-compare-value track-value)))))
+ local-fields
+ :initial-value nil))))
+
+(defvar emms-filters-string-field-names
+ '(info-albumartist
+ info-artist
+ info-composer
+ info-performer
+ info-title
+ info-album
+ info-date
+ info-originaldate
+ info-note
+ info-genre)
+ "The list of track field names that are strings.")
+
+(emms-filters-register-filter-factory
+ "Fields search"
+ 'emms-filters-make-filter-fields-search
+ `(("Choose fields to search : "
+ (:list . ,emms-filters-string-field-names))
+ ("Search: " (:string . nil))))
+
+;; field-compare
+;; -------------
+(defvar emms-filters-number-field-names
+ '(info-tracknumber
+ info-discnumber
+ info-year
+ info-originalyear
+ info-originaldate
+ info-playing-time)
+ "The list of track field names that are numbers.")
+
+(defvar emms-filters-string-compare-functions
+ '(emms-filters-match-string
+ string-equal-ignore-case
+ string=
+ string<
+ string>
+ string-match)
+ "Compare functions for filter creation.")
+
+(defvar emms-filters-number-compare-functions
+ '(> >= = <= <)
+ "Compare functions for filter creation.")
+
+(defvar emms-filters-track-types
+ '(file url stream streamlist playlist)
+ "Types of tracks we can have.")
+
+(defun emms-filters-match-string (string1 string2)
+ "Check to see if STRING2 is in STRING1.
+
+This is the inverse parameter list of `string-match'.
+So we can continue with the language of
+`filter track where field contains string'
+`filter track where field > value'."
+ (string-match string2 string1))
+
+(defun emms-filters-make-filter-field-compare (operator-func field compare-val)
+ "Make a filter that compares FIELD to COMPARE-VALUE with OPERATOR-FUNC.
+Works for number fields and string fields provided the appropriate
+type match between values and the comparison function. Partials can
+easily make more specific factory functions from this one."
+ (let ((local-operator operator-func)
+ (local-field field)
+ (local-compare-val compare-val))
+ #'(lambda (track)
+ (let ((track-val (emms-track-get track local-field)))
+ (and
+ track-val
+ (funcall local-operator local-compare-val track-val))))))
+
+;; not sure anyone will use these directly but you never know.
+;; Its a good test for the prompting system.
+;; Note the use of ` and , to resolve the selection lists here.
+(emms-filters-register-filter-factory "Number field compare"
+ 'emms-filters-make-filter-field-compare
+ ;; prompts
+ `(("Compare Function: "
+ (:function . ,emms-filters-number-compare-functions))
+ ("Field name: "
+ (:symbol . ,emms-filters-number-field-names))
+ ("Compare to: "
+ (:number . nil))))
+
+(emms-filters-register-filter-factory "String field compare"
+ 'emms-filters-make-filter-field-compare
+ ;; prompts
+ `(("Compare Function: "
+ (:function . ,emms-filters-string-compare-functions ))
+ ("Field name: "
+ (:symbol . ,emms-filters-string-field-names))
+ ("Compare to: "
+ (:string . nil))))
+
+;; Generic field comparison factories.
+;; parameter order is good for making partials.
+(emms-filters-register-filter-factory
+ "Duration less"
+ (apply-partially 'emms-filters-make-filter-field-compare
+ '<= 'info-playing-time)
+ '(("Duration: " (:number . nil))))
+
+(emms-filters-register-filter-factory
+ "Duration more"
+ (apply-partially 'emms-filters-make-filter-field-compare
+ '>= 'info-playing-time)
+ '(("Duration: " (:number . nil))))
+
+(emms-filters-register-filter-factory
+ "Genre"
+ (apply-partially 'emms-filters-make-filter-field-compare
+ 'string-equal-ignore-case 'info-genre)
+ '(("Genre: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Track type"
+ (apply-partially 'emms-filters-make-filter-field-compare
+ 'eq 'type)
+ '(("Track type: "
+ (:string . '(file url stream streamlist playlist)))))
+
+;; Search fields for text. Same behavior as emms-browser-search.
+;; Replace the emms browser searches with these filter factories.
+
+(emms-filters-register-filter-factory
+ "Album-artist"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-albumartist))
+ '(("Search album artist: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Artist"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-artist))
+ '(("Search artist: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Artists"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-artist info-albumartist))
+ '(("Search artists: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Artists and composer"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-artist info-albumartist info-composer))
+ '(("Search artists and composer: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Album"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-album))
+ '(("Search album: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Title"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-title))
+ '(("Search title: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Performer"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-performer))
+ '(("Search performer: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Orchestra"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-orchestra))
+ '(("Search orchestra: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Composer"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-composer))
+ '(("Search composer: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Notes"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-note))
+ '(("Search notes: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Titles"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-title
+ info-album))
+ '(("Search titles: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Names"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-albumartist
+ info-name
+ info-artist
+ info-composer
+ info-performer))
+ '(("Search names: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "Names and titles"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-albumartist
+ info-artist
+ info-composer
+ info-performer
+ info-name
+ info-title
+ info-album))
+ '(("Search names and titles: " (:string . nil))))
+
+(emms-filters-register-filter-factory
+ "All text"
+ (apply-partially 'emms-filters-make-filter-fields-search
+ '(info-albumartist
+ info-artist
+ info-composer
+ info-performer
+ info-title
+ info-album
+ info-name
+ info-date
+ info-originaldate
+ info-note
+ info-genre))
+ '(("Search all text fields: " (:string . nil))))
+
+;; Multi-filter - Just another factory.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; A filter of filters. A list of lists of filter Names and maybe a :not.
+;; Each list is Reduced with Or then reduced together with And and Not.
+(defun emms-filters-or-group->multi-funcs (filter-name-list)
+ "Return a list of functions from emms-filters-filters for a FILTER-NAME-LIST.
+Functions already in the list will be passed through."
+ (mapcar (lambda (filter-name)
+ (emms-filters-find-filter-function filter-name))
+ filter-name-list))
+
+(defun emms-filters-meta-filter->multi-funcs (meta-filter)
+ "Return a list of functions from emms-filters-filters for a META-FILTER."
+ (mapcar (lambda (or-group)
+ (emms-filters-or-group->multi-funcs or-group))
+ meta-filter))
+
+(defun emms-filters-reduce-or-group (or-group track)
+ "Reduce OR-GROUP for TRACK."
+ (cl-reduce
+ (lambda (result filter-func)
+ (or result
+ (funcall filter-func track)))
+ or-group
+ :initial-value nil))
+
+(defun emms-filters-reduce-invert-or-group (or-group track)
+ "Call an OR-GROUP list of filters with TRACK and reduce result with OR.
+If the first item is :not then invert the result from the reduction."
+ (let* ((invert (eq (car or-group) :not))
+ (group (if invert
+ (cdr or-group)
+ or-group))
+ (result (emms-filters-reduce-or-group group track)))
+ (if invert (not result) result)))
+
+(defun emms-filters-make-multi-filter (meta-filter)
+ "Make a track filter function from META-FILTER.
+The function will take a track as a parameter and return t if the track
+does not match the filters.
+A multi-filter is a list of lists of filter names.
+The track is checked against each filter, each list of filters is
+reduced with or. The lists are reduced with and.
+Returns True if the track should be filtered out."
+ (let ((local-multi-funcs
+ (emms-filters-meta-filter->multi-funcs meta-filter)))
+ #'(lambda (track)
+ (cl-reduce
+ (lambda (result funclist)
+ (and result
+ (emms-filters-reduce-invert-or-group funclist track)))
+ local-multi-funcs
+ :initial-value t))))
+
+(emms-filters-register-filter-factory "Multi-filter"
+ 'emms-filters-make-multi-filter
+ '(nil))
+
+;;; Some filters.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; A simple not a filter, So we have a default of no filters to choose/return to.
+(emms-filters-register-filter "No filter" nil)
+
+;; The variables are simply organizational so they can
+;; be created and added to the filter ring.
+
+;; factory name factory arg
+(defvar emms-filters-decade-filters
+ '(("Year range" "1900s" 1900 1909)
+ ("Year range" "1910s" 1910 1919)
+ ("Year range" "1920s" 1920 1929)
+ ("Year range" "1930s" 1930 1939)
+ ("Year range" "1940s" 1940 1949)
+ ("Year range" "1950s" 1950 1959)
+ ("Year range" "1960s" 1960 1969)
+ ("Year range" "1970s" 1970 1979)
+ ("Year range" "1980s" 1980 1989)
+ ("Year range" "1990s" 1990 1999)
+ ("Year range" "2000s" 2000 2009)
+ ("Year range" "2010s" 2010 2019)
+ ("Year range" "2020s" 2020 2029))
+ "filter tracks by decade")
+
+(defvar emms-filters-genre-filters
+ '(("Genre" "Waltz" "waltz")
+ ("Genre" "Vals" "vals")
+ ("Genre" "Tango" "tango")
+ ("Genre" "Milonga" "milonga")
+ ("Genre" "Condombe" "condombe")
+ ("Genre" "Salsa" "salsa")
+ ("Genre" "Blues" "blues")
+ ("Genre" "Rock" "rock")
+ ("Genre" "Swing" "swing")
+ ("Genre" "Pop" "pop")
+ ("Genre" "Rap" "rap")
+ ("Genre" "Hip hop" "hip hop")
+ ("Genre" "Classical" "classical")
+ ("Genre" "Baroque" "baroque")
+ ("Genre" "Chamber" "chamber")
+ ("Genre" "Reggae" "reggae")
+ ("Genre" "Folk" "folk")
+ ("Genre" "World" "world")
+ ("Genre" "Metal" "metal")
+ ("Genre" "Fusion" "fusion")
+ ("Genre" "Jazz" "jazz"))
+ "Some filters for a the track genre")
+
+(defvar emms-filters-last-played-filters
+ '(("Played since" "Played in the last month" 30)
+ ("Not played since" "Not played since a year" 365))
+ "filters for the last time a track was played")
+
+(defvar emms-filters-track-type-filters
+ '(("Track type" "File" file)
+ ("Track type" "Url" url)
+ ("Track type" "Stream" stream)
+ ("Track type" "Stream list" streamlist)
+ ("Track type" "Play list" playlist))
+ "filters for track types")
+
+(defvar emms-filters-duration-filters
+ '(("Duration less" "Duration <1 min" 60)
+ ("Duration less" "Duration <5 min" 300)
+ ("Duration more" "Duration >5 min" 300)
+ ("Duration more" "Duration >10 min" 600))
+ "filters for the duration of a track.")
+
+(defun emms-filters-make-default-filters()
+ "Make some default filters anyone would not mind having."
+ (emms-filters-make-filters emms-filters-decade-filters)
+ (emms-filters-make-filters emms-filters-genre-filters)
+ (emms-filters-make-filters emms-filters-track-type-filters)
+ (emms-filters-make-filters emms-filters-last-played-filters)
+ (emms-filters-make-filters emms-filters-duration-filters))
+
+;; Install some default filters.
+(emms-filters-make-default-filters)
+
+;; The Meta-Filter stack
+;; An interactive multi-filter stack.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; The current filter is the multi-filter version of the meta-filter
+;; at the top of the filter stack.
+;;
+;; Adding more filters to the current filter pushes a new filter to the stack.
+;; emms-filters-pop pops the stack, returning to the last filter.
+;;
+;; Other filters can be added to the current filter
+;; with 'and', 'or' as well as 'and-not' and 'smash' filter selections.
+
+(defun emms-filters-current-meta-filter ()
+ "Return the current meta-filter from the top of the stack."
+ (format "%S" (car emms-filters-stack)))
+
+(defun emms-filters-copy-meta-filter (filter)
+ "Copy the meta-filter given by FILTER."
+ (mapcar 'copy-sequence filter))
+
+(defun emms-filters-filter-name->meta-filter (filter-name)
+ "Make a meta filter cons from a FILTER-NAME."
+ (cons filter-name
+ (list (list filter-name))))
+
+(defun emms-filters-format-meta-filter-groups (filter-list)
+ "Format the FILTER-LIST contents to a list of strings."
+ (mapconcat (lambda (fname) (format "%s " fname)) filter-list " | "))
+
+(defun emms-filters-make-name (meta-filter)
+ "Construct a name from the META-FILTER contents."
+ (mapconcat 'identity
+ (mapcar 'emms-filters-format-meta-filter-groups meta-filter) " && "))
+
+(defun emms-filters-make-filter-cons-from-meta-filter (filter)
+ "Make a filter cons from meta-filter FILTER."
+ (cons (emms-filters-make-name filter) filter))
+
+(defun emms-filters-set-filter (filter)
+ "Set the current filter to FILTER.
+Filter should be a filter cons in the form of `(name . function)."
+ (setq emms-filters-current-filter-name (car filter))
+ (setq emms-filters-current-filter filter))
+
+;; (defun emms-filters-browse-by ()
+;; "The single interface to emms-browser. Re-render please.
+;; Uses the top level type, or the default browse type."
+;; (emms-browse-by (or emms-browser-top-level-type
+;; emms-browser-default-browse-type)))
+
+(defun emms-filters-refilter ()
+ "Make a multi-filter function from the current meta-filter and set it.
+Run the filter changed hooks. Ask the Browser/renderer to re-render with
+the render and expand hooks."
+ (emms-filters-set-filter (cons (caar emms-filters-stack)
+ (emms-filters-make-multi-filter (cdar emms-filters-stack))))
+
+ ;; filter-changed-hook is a defcustom for users.
+ (run-hooks 'emms-filters-filter-changed-hook)
+ ;; this hook is for renderers.
+ (run-hooks 'emms-filters-make-and-render-hash-hook)
+ ;; If it is a search ora filter expand the results.
+ (when (or emms-filters-stack emms-filters-search-caches)
+ (run-hooks 'emms-filters-expand-render-hook)))
+
+(defun emms-filters-ensure-metafilter (filter)
+ "Ensure that FILTER is a meta-filter."
+ (cond ((stringp filter) ; name
+ (emms-filters-filter-name->meta-filter filter))
+ ((functionp (cdr filter)) ; filter function
+ (emms-filters-filter-name->meta-filter (car filter)))
+ ;; meta-filter - cdr is a listp
+ (t filter)))
+
+(defun emms-filters-push (&optional filter)
+ "Push a copy of FILTER to the meta-filter stack.
+Should be of the form (filter-name . metafilter/filter)
+or a filter-name.
+
+If filter is not supplied select a filter from the
+ list of filter functions or create a new one.
+
+Make a filter function and set it. If it is a name,
+look it up in our filter list. If it is a function, make
+it a meta-filter, if it is a meta-filter use it."
+ (interactive)
+ (let ((fname (or filter (emms-filters-choose-filter))))
+ (push (emms-filters-ensure-metafilter fname)
+ emms-filters-stack)
+ (emms-filters-refilter)))
+
+;;; base functions
+(defun emms-filters-current-meta-filter-name ()
+ "Give the constructed name of the current filter."
+ (emms-filters-make-name (cdar emms-filters-stack)))
+
+(defun emms-filters-clear ()
+ "Clear the meta filter stack and the current filter function."
+ (interactive)
+ (setq emms-filters-stack nil)
+ (emms-filters-refilter))
+
+(defun emms-filters-clear-all ()
+ "Reset the cache stack, the filter stack and the filter-ring."
+ (interactive)
+ (emms-filters-clear)
+ (emms-filters-clear-caches)
+ (emms-filters-clear-ring-filter))
+
+(defun emms-filters-pop ()
+ "Pop the stack, set the current filter function and re-render."
+ (interactive)
+ (pop emms-filters-stack)
+ (emms-filters-refilter))
+
+(defun emms-filters-swap ()
+ "Reverse the last two entries in the stack."
+ (interactive)
+ (let* ((current (pop emms-filters-stack))
+ (previous (pop emms-filters-stack)))
+ (push current emms-filters-stack)
+ (push previous emms-filters-stack)
+ (emms-filters-refilter)))
+
+(defun emms-filters-swap-pop ()
+ "Swap and pop the stack."
+ (interactive)
+ (let* ((current (pop emms-filters-stack)))
+ (pop emms-filters-stack)
+ (push current emms-filters-stack)))
+
+(defun emms-filters-squash ()
+ "Squash the stack, keep the top."
+ (interactive)
+ (let* ((current (pop emms-filters-stack)))
+ (setq emms-filters-stack nil)
+ (push current emms-filters-stack)))
+
+(defun emms-filters-append-string-to-file (string filename)
+ "Append STRING to FILENAME."
+ (interactive)
+ (append-to-file string nil filename))
+
+(defun emms-filters-format-multi-filter (meta-filter)
+ "Format the META-FILTER as Lisp code to use with `emms-filters-make-filters'."
+ (format "(\"Multi-filter\"\n %S\n %S)\n\n"
+ (car meta-filter)
+ (cdr meta-filter)))
+
+(defun emms-filters-save-meta-filter (meta-filter)
+ "Save the META-FILTER to the `emms-filters-multi-filter-save-file' if set."
+ (when emms-filters-multi-filter-save-file
+ (append-to-file
+ (emms-filters-format-multi-filter meta-filter)
+ nil emms-filters-multi-filter-save-file)))
+
+(defun emms-filters-keep ()
+ "Register the current filter into the list of filters for the
+ session. If emms-filters-multi-filter-save-file is set, append the
+ filter definition there."
+ (interactive)
+ (message "Registering the current meta-filter as a filter for the session")
+ (emms-filters-status)
+
+ (when (and emms-filters-stack (consp (car emms-filters-stack)))
+ (emms-filters-save-meta-filter (car emms-filters-stack))
+ (emms-filters-register-filter (caar emms-filters-stack)
+ (emms-filters-make-multi-filter (cdar emms-filters-stack)))
+ (emms-filters-add-to-filter-menu "Kept filters" (caar emms-filters-stack))))
+
+(defun emms-filters-hard-filter ()
+ "A hard save of filtered results.
+Build a cache of filtered tracks from the current cache
+filtered by the current filters.
+
+Emulates a search, pushing a new cache on the cache stack.
+This cache is the same as all the rest and emms-cache-db.
+
+See also: ems-pop-cache."
+ (interactive)
+ (let* ((search-name (emms-filters-full-name))
+
+ (search-cache (make-hash-table
+ :test (if (fboundp 'define-hash-table-test)
+ 'string-hash
+ 'equal))))
+ (maphash (lambda (path track)
+ (when (emms-filters-browser-filter-hook-function track)
+ (puthash path track search-cache)))
+ (emms-filters-last-search-cache))
+
+ (emms-filters-push-cache search-name search-cache))
+ (emms-filters-refilter))
+
+(defun emms-filters-choose-filter-recursive (&optional choices)
+ "Choose a filter from emms-filters-filter-menu tree or the alist given
+ as CHOICES. Requires that the lists of filter names be lists of cons
+ (name . name). Allows for tree structures of any depth."
+ (let* ((choices (or choices emms-filters-filter-menu))
+ (choice (assoc (completing-read
+ "Choose a filter or group:" choices nil t)
+ choices)))
+ (if (consp choice)
+ (emms-filters-choose-filter-recursive (cadr choice))
+ (if (string= "new filter" choice)
+ (emms-filters-new-filter))
+ choice)))
+
+(defun emms-filters-choose-filter ()
+ "Choose a filter from our filter menu tree.
+Stupid, Assumes our tree is an alist of lists of strings."
+ (let* ((choice (completing-read
+ "Choose a filter group:" emms-filters-filter-menu nil t))
+ (newlist (cadr (assoc choice emms-filters-filter-menu))))
+ (if newlist
+ (completing-read "Choose a filter:" newlist nil t)
+ (if (string= "new filter" choice)
+ (emms-filters-new-filter)
+ choice))))
+
+(defun emms-filters-choose-factory ()
+ "Choose a filter factory from our list of factories."
+ (completing-read
+ "Choose a filter factory:"
+ (mapcar (lambda (factory)
+ (when (car (cddr factory))
+ factory))
+ emms-filters-filter-factories)
+ nil t))
+
+(defun emms-filters-one-shot (&optional filter-name)
+ "Push FILTER-NAME given onto the filter stack,
+hard filter to create a cache, Then pop the filter.
+
+If not given, Select or create a filter from the list of filter functions.
+The filter will be used to create a new entry on the
+cache stack and will be added to the filter menu.
+
+Steps are;
+ 1. Take, Create, or choose a filter,
+ 2. Push filter,
+ 3. Push cache with filter,
+ 4. Pop filter.
+If a filter was created it will remain as a filter choice for the session.
+This is like browser-search, but with more choices.
+"
+ (interactive)
+ (let ((fname
+ (or filter-name
+ (emms-filters-choose-filter))))
+ (emms-filters-push fname)
+ (emms-filters-hard-filter)
+ (emms-filters-pop)))
+
+(defun emms-filters-quick-one-shot (factory-name)
+ "Create a new filter from FACTORY-NAME, using a generated filter name.
+Push the filter, push the resulting cache, then pop.
+Leaving a new cache on the search stack. And the filter stack as it was.
+The filter will rest under the factory name filter menu for the session.
+This imitates the emms browser search."
+ (interactive)
+ (emms-filters-one-shot (emms-filters-new-filter factory-name t)))
+
+(defun emms-filters-smash ()
+ "Clear the stack and Select a filter from the list of filter functions."
+ (interactive)
+ (emms-filters-clear)
+ (let ((fname (emms-filters-choose-filter)))
+ (emms-filters-push fname)))
+
+(defun emms-filters-push-or (filter-name meta-filter)
+ "Push a new Or with FILTER-NAME to the last Or group in the META-FILTER."
+ (let* ((rev-mf (reverse (emms-filters-copy-meta-filter meta-filter)))
+ (rest-mf (reverse (cdr rev-mf))))
+ (append rest-mf
+ (list (append (car rev-mf) (list filter-name))))))
+
+(defun emms-filters-or ()
+ "Add filter to current/last filter list in the current filter.
+Creates an `OR' filter."
+ (interactive)
+ (let ((fname (emms-filters-choose-filter)))
+ (emms-filters-push
+ (emms-filters-make-filter-cons-from-meta-filter
+ (emms-filters-push-or fname (emms-filters-copy-meta-filter (cdar emms-filters-stack)))))))
+
+(defun emms-filters-push-and (filter-name filter)
+ "Push a new And list with FILTER-NAME onto FILTER."
+ (append filter (list (list filter-name))))
+
+(defun emms-filters-and ()
+ "Select a filter to start a new list of filters.
+Creates a new `AND' list of filters."
+ (interactive)
+ (let ((fname (emms-filters-choose-filter)))
+ (emms-filters-push
+ (emms-filters-make-filter-cons-from-meta-filter
+ (emms-filters-push-and fname (emms-filters-copy-meta-filter (cdar emms-filters-stack)))))))
+
+(defun emms-filters-and-not ()
+ "Select a filter to start a new list of filters.
+Creates a new `AND-NOT' list of filters."
+ (interactive)
+ (let ((fname (emms-filters-choose-filter)))
+ (emms-filters-push
+ (emms-filters-make-filter-cons-from-meta-filter
+ (emms-filters-push-or fname
+ (emms-filters-push-and ':not (emms-filters-copy-meta-filter (cdar emms-filters-stack))))))))
+
+(defun emms-filters-format-stack()
+ "Print the stack."
+ (format "\t%s" (mapconcat 'car emms-filters-stack "\n\t")))
+
+(defun emms-filters-full-name ()
+ "Give a full name for the current filtering. Includes the ring
+ filter name plus current filter name. Does not show the current cache
+ name. Only show the ring filter name if its function is not nil. Use
+ the current filter name so that `no filter' shows."
+ (let ((ring (when (cdr emms-filters-current-ring-filter)
+ (car emms-filters-current-ring-filter)))
+ (current (car emms-filters-current-filter)))
+ (cond ((and ring current)
+ (format "%s : %s" ring current))
+ (ring ring)
+ (current current)
+ (t nil))))
+
+(defun emms-filters-status ()
+ "Format what we know into something readable."
+ (interactive)
+ (format "Ring: %s\nMeta Filter: %s\nFilter stack:\n%s\nCache stack:\n %s"
+ (car emms-filters-current-ring-filter)
+ (emms-filters-current-meta-filter)
+ (emms-filters-format-stack)
+ (emms-filters-format-cache-stack)))
+
+(defun emms-filters-status-print ()
+ "Print what we know."
+ (interactive)
+ (message (emms-filters-status)))
+
+(defun emms-filters-set-ring-filter (filter-name)
+ "Given a FILTER-NAME set the current ring filter and re-render."
+ (setq emms-filters-current-ring-filter
+ (assoc filter-name emms-filters-filters))
+ (emms-filters-refilter))
+
+(defun emms-filters-clear-ring-filter ()
+ "Set the ring filter to no filter."
+ (interactive)
+ (emms-filters-set-ring-filter "no filter"))
+
+(defun emms-filters-current-ring-filter-name ()
+ "The current ring filter name, more descriptive than car."
+ (if emms-filters-current-ring-filter
+ (car emms-filters-current-ring-filter)
+ "no filter"))
+
+(defun emms-filters-next-ring-filter()
+ "Move to the next filter in the filter ring."
+ (interactive)
+ (emms-filters-set-ring-filter
+ (ring-next emms-filters-filter-ring
+ (if emms-filters-current-ring-filter
+ (car emms-filters-current-ring-filter)
+ (ring-ref emms-filters-filter-ring 0)))))
+
+(defun emms-filters-previous-ring-filter()
+ "Move to the previous filter in the filter ring."
+ (interactive)
+ (emms-filters-set-ring-filter
+ (ring-previous emms-filters-filter-ring
+ (if emms-filters-current-ring-filter
+ (car emms-filters-current-ring-filter)
+ (ring-ref emms-filters-filter-ring 0)))))
+
+;; --------------------------------------------------
+;; Searching
+;; --------------------------------------------------
+;;; The Search Cache Stack
+;;
+;; The search cache stack is a simply a stack of emms-cache-db style hash tables.
+;; Each entry is a subset of the master emms-cache-db created through filtering.
+;; Their names are constructed from the filters which created them.
+;;
+;; Filtering and displaying of tracks is done against the top cache on the stack.
+;;
+;; A cache of the current filter results can be pushed to the cache stack at any
+;; time with hard-filter. These results will reflect the current meta-filter
+;; as well as the filter currently chosen in the filter ring.
+;;
+;; A one-shot filter combined with a hard filter is emms-filters-quick-one-shot.
+;; This effectively emulates the former emms-browser search behavior of
+;; filtering and saving a cache by pushing a filter, hard-filter, pop.
+
+
+
+(defun emms-filters-push-cache (&optional filter-name cache)
+ "Cache/Store FILTER-NAME and CACHE in a stack.
+If FILTER-NAME and CACHE are not present, interactively,
+allow selection of a cache from the cache stash."
+ (interactive)
+ (if (and filter-name cache)
+ (push (cons filter-name cache) emms-filters-search-caches)
+ (let ((stashed-cache
+ (assoc (completing-read "Select Cache"
+ emms-filters-cache-stash nil t)
+ emms-filters-cache-stash)))
+ (push stashed-cache emms-filters-search-caches)))
+ (emms-filters-refilter))
+
+(defun emms-filters-stash-cache ()
+ "Stash the current-cache for later."
+ (interactive)
+ (push (car emms-filters-search-caches) emms-filters-cache-stash))
+
+(defun emms-filters-stash-pop-cache ()
+ "Stash the current-cache for later, pop it from the stack."
+ (interactive)
+ (emms-filters-stash-cache)
+ (emms-filters-pop-cache))
+
+(defun emms-filters-get-search-keys ()
+ "Return the search-list keys for the current search cache."
+ (if (< 0 (length emms-filters-search-caches))
+ (mapcar #'car emms-filters-search-caches)
+ '()))
+
+(defun emms-filters-current-cache-name ()
+ "Return the name of the current search cache."
+ (car (reverse (emms-filters-get-search-keys))))
+
+(defun emms-filters-format-search-list (search-list)
+ "Create a string format of a SEARCH-LIST.
+Search lists are what is used by the old emms-browser search function,
+or the emms-filters-filter-factory `search-fields'."
+ (let ((infos (append (car (car search-list))))
+ (svalue (cdar search-list)))
+ (format "%s - %s"
+ (mapconcat
+ #'(lambda (info)
+ (if (symbolp info)
+ (substring (symbol-name info) 5)
+ info))
+ infos " | ")
+ svalue)))
+
+(defun emms-filters-format-cache-stack ()
+ "Create a list of search crumb strings for the current search cache."
+ (format "\t%s" (mapconcat #'identity (emms-filters-get-search-keys) " \n\t")))
+
+(defun emms-filters-show-cache-stack ()
+ "Message the current search cache stack."
+ (interactive)
+ (message "Emms Cache stack:\n %s\n"
+ (mapconcat #'identity (emms-filters-get-search-keys) "\n ")))
+
+(defun emms-filters-show-cache-stash ()
+"Show the cache names in the stash."
+(interactive)
+(message "Emms cache stash:\n %s\n"
+ (mapconcat 'identity
+ (reverse (mapcar #'car emms-filters-cache-stash))
+ "\n ")))
+
+(defun emms-filters-last-search-cache ()
+ "Return the cache portion of the last search cache entry."
+ (if (< 0 (length emms-filters-search-caches))
+ (cdar emms-filters-search-caches)
+ emms-cache-db))
+
+(defun emms-filters-pop-cache ()
+ "Pop the search results cache and then render to show the previous search result."
+ (interactive)
+ (pop emms-filters-search-caches)
+ (emms-filters-refilter))
+
+(defun emms-filters-clear-caches ()
+ "Clear the cache stack."
+ (interactive)
+ (setq emms-filters-search-caches nil)
+ (emms-filters-refilter))
+
+(defun emms-filters-swap-cache ()
+ "Swap / reverse the last two entries in the cache stack."
+ (interactive)
+ (let* ((current (pop emms-filters-search-caches))
+ (previous (pop emms-filters-search-caches)))
+ (push current emms-filters-search-caches)
+ (push previous emms-filters-search-caches)
+ (emms-filters-refilter)))
+
+(defun emms-filters-swap-pop-cache ()
+ "Swap and pop the cache stack."
+ (interactive)
+ (let* ((current (pop emms-filters-search-caches)))
+ (pop emms-filters-search-caches)
+ (push current emms-filters-search-caches)))
+
+(defun emms-filters-squash-caches ()
+ "Squash the cache stack, keep the top entry."
+ (interactive)
+ (let* ((current (pop emms-filters-search-caches)))
+ (setq emms-filters-search-caches nil)
+ (push current emms-filters-search-caches)))
+
+(defun emms-filters-search-stack-size ()
+ "Give the current length of the search cache stack."
+ (length emms-filters-search-caches))
+
+(defun emms-filters-is-filtering ()
+ "True if there is a search stack or a filter stack or a ring-filter."
+ (if (or (> (length emms-filters-search-caches) 0)
+ (> (length emms-filters-stack) 0)
+ (if emms-filters-current-ring-filter t nil))
+ t
+ nil))
+
+(defun emms-filters-empty-result-message ()
+ "Display some help if the results are empty."
+ (concat "No records match with the current search cache and filters.\n\n"
+ (format "Cache: %s\nRing: %s\nFilter: %s\n\nEMMS Cache size: %s \n"
+ (emms-filters-current-cache-name)
+ (emms-filters-current-ring-filter-name)
+ (car emms-filters-current-filter)
+ (hash-table-count emms-cache-db))
+ "
+You may have created a filter with no results found.
+If this is the case you may return to your previous
+filter by popping the current filter.
+
+You may also have an empty search cache on
+the cache stack, popping or stashing and popping
+the current searche cache may yield results.
+
+You may also have selected a filter
+in the filter ring which has no matches.
+Move your filter ring selection to 'no filter'
+or select a different filter for different results."))
+
+
+(defun emms-filters-search-by (filter-factory-name)
+ "Search using FILTER-FACTORY-NAME to create a filter.
+Emulating the browser search, build a filter using factory name
+and cache the results to the cache stack."
+ (interactive)
+ (emms-filters-quick-one-shot filter-factory-name))
+
+;; replacements for emms-browser search and then some.
+(defun emms-filters-search-by-albumartist ()
+ "A fields search quick one shot for Album Artist."
+ (interactive)
+ (emms-filters-quick-one-shot "Album-artist"))
+
+(defun emms-filters-search-by-artist ()
+ "A fields search quick one shot for Artist."
+ (interactive)
+ (emms-filters-quick-one-shot "Artist"))
+
+(defun emms-filters-search-by-composer ()
+ "A fields search quick one shot for composer."
+ (interactive)
+ (emms-filters-quick-one-shot "Composer"))
+
+(defun emms-filters-search-by-performer ()
+ "A fields search quick one shot for performer."
+ (interactive)
+ (emms-filters-quick-one-shot "Performer"))
+
+(defun emms-filters-search-by-title ()
+ "A fields search quick one shot for title."
+ (interactive)
+ (emms-filters-quick-one-shot "Title"))
+
+(defun emms-filters-search-by-album ()
+ "A fields search quick one shot for album title."
+ (interactive)
+ (emms-filters-quick-one-shot "Album"))
+
+(defun emms-filters-search-by-titles ()
+ "A fields search quick one shot for album and song titles."
+ (interactive)
+ (emms-filters-quick-one-shot "Titles"))
+
+(defun emms-filters-search-by-names-and-titles ()
+ "A fields search quick one shot for all names and titles."
+ (interactive)
+ (emms-filters-quick-one-shot "Names and titles"))
+
+(defun emms-filters-search-by-names ()
+ "A fields search quick one shot for all names."
+ (interactive)
+ (emms-filters-quick-one-shot "Names"))
+
+(defun emms-filters-search-by-all-text ()
+ "A fields search quick one shot for All text fields."
+ (interactive)
+ (emms-filters-quick-one-shot "All text"))
+
+
+;;; Testing
+;;; -------------------------------------------------------------------
+;;; Some convenience functions to make it easy to test a filter.
+
+(defun emms-filters-test-get-track-samples (cache &optional drop take)
+ "Return a list of tracks from the CACHE, DROP tracks then TAKE as indicated.
+Will drop 0 and take 1O by default."
+ (let* ((tracks (list))
+ (drop (or drop 0))
+ (take (+ (or take 10) drop))
+ (counter 0))
+ (maphash (lambda (_path track)
+ (when
+ (and
+ (> counter drop)
+ (< counter take))
+ (push track tracks))
+ (setq counter (+ counter 1)))
+ cache)
+ tracks))
+
+(defun emms-filters-test-factory (factory-name parms track)
+ "Create and test filter from FACTORY-NAME and PARMS.
+Test it against TRACK."
+ (funcall
+ (emms-filters-make--filter factory-name parms)
+ track))
+
+(defun emms-filters-test-factory-interactive (factory-name track)
+ "Interactively create and test filter from FACTORY-NAME.
+Test it against TRACK."
+ (funcall
+ (emms-filters-new-filter factory-name t)
+ track))
+
+(defun emms-filters-test-filter-name (track filter-name &optional ring-filter-name)
+ "Test filters identified by FILTER_NAME and RING-FILTER-NAME against a TRACK."
+ (emms-filters-test-filter
+ track
+ (cdr (assoc filter-name emms-filters-filters))
+ (if ring-filter-name
+ (cdr (assoc ring-filter-name emms-filters-filters))
+ nil)))
+
+(defun emms-filters-test-filter (track filter &optional ring-filter)
+ "Test TRACK against FILTER and optional RING-FILTER.
+A functional equivalent to the emms-filters-browser-hook function.
+First we test the track against the ring-filter, then we combine
+the result with the result of the filter."
+ (and (if ring-filter
+ (funcall ring-filter track)
+ t)
+ (if filter
+ (funcall filter track)
+ t)))
+
+(defun emms-filters-test-filter-tracks-direct (factory-name parms tracks)
+ "Test a list of TRACKS against a filter created from FACTORY-NAME and
+ PARMS. Uses emms-filters-test-factory directly rather than emulating the
+ browser-hook-function. Test it against some portion starting with START
+ records and stopping at STOP records of the existing cache-db. Returns
+ a list of cons with the filter result and the track."
+ (mapcar (lambda (track)
+ (cons
+ (emms-filters-test-factory factory-name parms track)
+ track))
+ tracks))
+
+(defun emms-filters-test-filter-tracks (factory-name parms tracks)
+ "Test a list of TRACKS against a filter created from FACTORY-NAME and PARMS.
+Emulates the browser-hook function by using emms-filters-test-filter.
+Test it against some portion starting with START records and stopping
+at STP records of the existing cache-db.
+Returns a list of cons with the filter result and the track."
+ (let ((filter (emms-filters-make--filter factory-name parms)))
+ (mapcar (lambda (track)
+ (cons
+ (emms-filters-test-filter track filter)
+ track))
+ tracks)))
+
+(defun emms-filters-test-filter-tracks-name (filter-name tracks)
+ "Test a list of TRACKS against a FILTER-NAME.
+Emulates the browser-hook function by using emms-filters-test-filter.
+Test it against some portion starting with START records and stopping
+at STP records of the existing cache-db.
+Returns a list of cons with the filter result and the track."
+ (let ((filter (cdr (assoc filter-name emms-filters-filters))))
+ (mapcar (lambda (track)
+ (cons
+ (emms-filters-test-filter-name track filter)
+ track))
+ tracks)))
+
+(defun emms-filters-test-find-tracks (cache filter)
+ "Return a list of tracks from the CACHE filtered by function FILTER."
+ (let* ((tracks (list)))
+ (maphash (lambda (_path track)
+ (when (funcall filter track)
+ (push track tracks)))
+ cache)
+ tracks))
+
+(defun emms-filters-test-find-tracks-with-name (cache filter-name)
+ "Return a list of tracks from the CACHE filtered by function FILTER."
+ (let* ((tracks (list)))
+ (maphash (lambda (_path track)
+ (when (funcall (cdr (assoc filter-name emms-filters-filters)) track)
+ (push track tracks)))
+ cache)
+ tracks))
+
+;; ;;; Testing
+;; ;;; Some actual testing.
+;; ;;; Some sample tracks to use for data.
+;; (setq emms-filters-test-tracks
+;; '((*track* (type . file)
+;; (name . "/Someone/Some-album/Some-song/track0001")
+;; (info-playing-time . 180)
+;; (info-discnumber . "1")
+;; (info-artist . "Someone-else")
+;; (info-title . "Some-song")
+;; (info-tracknumber . "01")
+;; (info-album . "Some-album")
+;; (info-albumartist . "Someone")
+;; (info-year . 1940)
+;; (info-genre . "vals"))
+;; (*track* (type . file)
+;; (name . "/Another-one/Another-album/Another-song/track0002")
+;; (info-playing-time . 180)
+;; (info-discnumber . "1")
+;; (info-artist . "Another-Someone-else")
+;; (info-title . "Another-song")
+;; (info-tracknumber . "02")
+;; (info-album . "Another-album")
+;; (info-albumartist . "Another-one")
+;; (info-year . 1935)
+;; (info-genre . "tango"))))
+
+;; (defun pretty-cons (cons-list)
+;; "pretty print a list of cons."
+;; (mapconcat (lambda (str) (format "%s\n" str))
+;; cons-list))
+
+;; (defun emms-filters-do-tests ()
+;; "A function for isolating and running some tests."
+;; ;; Make some sample data from the first few tracks from the cache.
+;; (let ((emms-filters-test-tracks-sample
+;; (emms-filters-test-get-track-samples emms-cache-db))
+;; (first-test-track (car emms-filters-test-tracks))
+;; (second-test-track (cadr emms-filters-test-tracks)))
+
+;; A direct use of the generated filter.
+
+;; ;; Create a filter from a factory and test it against a single track.
+;; (emms-filters-test-factory "Genre" '("vals") first-test-track)
+;; (emms-filters-test-factory "Genre" '("vals") second-test-track)
+
+;; (emms-filters-test-factory "Titles" "Some" first-test-track)
+;; (emms-filters-test-factory "Titles" "Some" second-test-track)
+
+;; ;; Test a few tracks against it.
+;; (pretty-cons (emms-filters-test-filter-tracks "Genre" '("vals") emms-filters-test-tracks))
+;; (pretty-cons (emms-filters-test-filter-tracks "Genre" '("vals") emms-filters-test-tracks-sample))
+;; (pretty-cons (emms-filters-test-filter-tracks "Titles" '("Some") emms-filters-test-tracks))
+;; (pretty-cons (emms-filters-test-filter-tracks "Titles" '("Some") emms-filters-test-tracks-sample))
+;; (pretty-cons (emms-filters-test-filter-tracks "Titles" '("Viv") emms-filters-test-tracks-sample))
+
+;; (emms-filters-test-find-tracks emms-cache-db (emms-filters-make--filter "Titles" '("sollo")))
+
+;; ;; Test interactive creation of a filter from a factory.
+;; ;; create a filter from a factory and test it against a single track.
+;; (emms-filters-test-factory-interactive "Genre" first-test-track)
+;; (emms-filters-test-factory-interactive "Titles" first-test-track)))
+
+;; Testing Backward compatibility with the emms-browser.
+;; -------------------------------------------------------
+;; Make some old style browser filters to test
+;; the filter-ring backward compatibility.
+;; Steps to test:
+;; 1. Make some old style emms-browser filters,
+;; 3. Try them out directly by name.
+;;
+;; emms-browser-make-filter now inverts the filter result
+;; for compatibility with emms-filters. The only interface to them
+;; were next and previous functions.
+;; That functionality is replicated with the emms-filters-filter-ring.
+
+;; (defun emms-browser-make-filter-genre (genre)
+;; "Make a filter by GENRE."
+;; (let ((filter (funcall emms-filters-make-filter-genre genre)))
+;; (lambda (track)
+;; (not (filter track)))))
+
+;; (defun emms-browser-make-filter-genre (genre)
+;; "Make a filter by GENRE."
+;; (lambda (track)
+;; (let ((info (emms-track-get track 'info-genre)))
+;; (not (and info (string-equal-ignore-case genre info))))))
+;;
+;; (emms-browser-make-filter "test-vals"
+;; (emms-browser-make-filter-genre "vals"))
+;; (emms-browser-make-filter "test-tango"
+;; (emms-browser-make-filter-genre "tango"))
+;; (emms-browser-make-filter "test-milonga"
+;; (emms-browser-make-filter-genre "milonga"))
+
+;; emms-filters-filter-ring
+;; (pretty-cons (emms-filters-test-filter-tracks-name "test-vals" emms-filters-test-tracks))
+
+(provide 'emms-filters)
+;;; emms-filters.el ends here.
generated by cgit v1.2.3 (git 2.39.1) at 2025年09月03日 22:19:20 +0000

AltStyle によって変換されたページ (->オリジナル) /