author | Yoni Rabkin <yrk@gnu.org> | 2025年07月12日 17:27:02 -0400 |
---|---|---|
committer | Yoni Rabkin <yrk@gnu.org> | 2025年07月12日 17:27:02 -0400 |
commit | 62eca2f5ee9e100564223a676014bf3d29efb31f (patch) | |
tree | f85b74ad88f9f1fe387342faf5cc0d43cb616914 | |
parent | e5f46561c3c140d774354f91d1e95c60039a1934 (diff) | |
parent | bd5e088932b64f499e4f51c53f176abd4b859895 (diff) | |
download | emms-62eca2f5ee9e100564223a676014bf3d29efb31f.tar.gz |
-rw-r--r-- | doc/emms.texinfo | 1068 | ||||
-rw-r--r-- | emms-browser.el | 585 | ||||
-rw-r--r-- | emms-filters.el | 2119 |
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. |