Repository: beetbox/beets

Issue:
mpdstats: Crash when searching by `last_played` field
### Problem

Running this command in verbose (`-vv`) mode:

``` sh
$ beet -vv list 'last_played:2015..'
```

Led to this problem:

```
user configuration: ~/.config/beets/config.yaml
data directory: ~/.config/beets
plugin paths: /usr/local/bin/wlg
Sending event: pluginload
inline: adding item field multidisc
artresizer: method is (2, (6, 9, 3))
thumbnails: using IM to write metadata
thumbnails: using GIO to compute URIs
library database: /net/nfs4/server/mediacentre/Music/Catalogs/beets_test.blb
library directory: /net/nfs4/server/mediacentre/Music/Library_test
Sending event: library_opened
Traceback (most recent call last):
  File "/usr/bin/beet", line 9, in <module>
    load_entry_point('beets==1.3.17', 'console_scripts', 'beet')()
  File "/usr/lib/python2.7/site-packages/beets/ui/__init__.py", line 1236, in main
    _raw_main(args)
  File "/usr/lib/python2.7/site-packages/beets/ui/__init__.py", line 1226, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/usr/lib/python2.7/site-packages/beets/ui/commands.py", line 1068, in list_func
    list_items(lib, decargs(args), opts.album)
  File "/usr/lib/python2.7/site-packages/beets/ui/commands.py", line 1063, in list_items
    for item in lib.items(query):
  File "/usr/lib/python2.7/site-packages/beets/dbcore/db.py", line 538, in _get_objects
    if not self.query or self.query.match(obj):
  File "/usr/lib/python2.7/site-packages/beets/dbcore/query.py", line 438, in match
    return all([q.match(item) for q in self.subqueries])
  File "/usr/lib/python2.7/site-packages/beets/dbcore/query.py", line 629, in match
    timestamp = float(item[self.field])
  File "/usr/lib/python2.7/site-packages/beets/dbcore/db.py", line 231, in __getitem__
    raise KeyError(key)
KeyError: u'last_played'
```
### Setup
- OS: Arch Linux
- Python version: 2.7.11
- beets version: 1.3.17
- Turning off plugins made problem go away (yes/no): Without pugins the command returns no results but without errors

My configuration (output of `beet config`) is:

``` yaml
directory: /net/nfs4/server/mediacentre/Music/Library_test
library: /net/nfs4/server/mediacentre/Music/Catalogs/beets_test.blb
plugins: fromfilename inline edit fetchart embedart zero rewrite importfeeds thumbnails fuzzy info missing random types wlg replaygain mpdstats smartplaylist mpdupdate duplicates badfiles smartplaylist play
pluginpath: /usr/local/bin/wlg
per_disc_numbering: yes
threaded: no  #disable for debugging
import:
    resume: yes
    quiet_fallback: skip
    none_rec_action: ask
    log: ~/.config/beets/beetslog.txt
    detail: yes
    write: yes
    copy: yes
    move: no
    autotag: yes
    link: no
    resume: ask
    incremental: no
    timid: no
paths:
    default: $albumartist/$album%aunique{albumartist album year,albumtype label catalognum albumdisambig} ($year)/%if{$multidisc,$disc}$track $title
    singleton: $artist/-Misc-/$track $title
    comp: Various Artists/$album%aunique{albumartist album year,albumtype label catalognum albumdisambig} ($year)/%if{$multidisc,$disc}$track $title
item_fields:
    multidisc: 1 if disctotal > 1 else 0
replace:
    '[\\/]': _
    '^\.': _
    '[\x00-\x1f]': _
    '\.$': ''
    '\s+$': ''
    '^\s+': ''
art_filename: folder
fetchart:
    auto: yes
    cautious: yes
    remote_priority: no
    minwidth: 300
    maxwidth: 500
    enforce_ratio: no
    sources: albumart amazon wikipedia coverart itunes #google
embedart:
    auto: yes
    maxwidth: 300
thumbnails:
    auto: yes
lyrics:
    auto: yes
    fallback: 'No lyrics found.'
    sources: musixmatch lyricwiki lyrics.com genius #google
replaygain:
    auto: yes
    backend: gstreamer
    overwrite: no
wlg:
    auto: yes
    force: yes
    count: 3
mpdstats:
    rating_mix: 0.75
ui:
    color: yes
musicbrainz:
    searchlimit: 5
match:
    strong_rec_thresh: 0.04
    medium_rec_thresh: 0.25
    rec_gap_thresh: 0.25
    max_rec:
        missing_tracks: medium
        unmatched_tracks: medium
    preferred:
        countries: ['US', 'GB|UK', 'AU', 'AR', 'ES']
zero:
    fields: genre
    update_database: true
importfeeds:
    formats: m3u_multi
    dir: /net/nfs4/server/mediacentre/Music/Playlists/
    relative_to: /net/nfs4/server/mediacentre/Music/Library_test/
smartplaylist:
    relative_to: /net/nfs4/server/mediacentre/Music/Library_test/
    playlist_dir: /net/nfs4/server/mediacentre/Music/Playlists/
    playlists:
        - name: 'loved_beets.m3u'
          query: 'loved:1'
types:
    loved: int
    play_count: int
```



Discussion:
Thanks! I can indeed reproduce this even with just `beet list last_played:2015` _when `mpdstats` is enabled_. The crash doesn't happen when it's disabled.

Yes, but even with it disabled I can't get it to produce any results even though I can list plenty of tracks with last_played dates that fall within the query.

Yes: with the plugin disabled, beets doesn't know the type of the field, so range queries won't work. You could consider re-declaring the type with the types plugin for now.


Resolving patch:
diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py
--- a/beets/dbcore/query.py
+++ b/beets/dbcore/query.py
@@ -628,6 +628,8 @@ def __init__(self, field, pattern, fast=True):
         self.interval = DateInterval.from_periods(start, end)
 
     def match(self, item):
+        if self.field not in item:
+            return False
         timestamp = float(item[self.field])
         date = datetime.utcfromtimestamp(timestamp)
         return self.interval.contains(date)


Test patch:
diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py
--- a/test/test_types_plugin.py
+++ b/test/test_types_plugin.py
@@ -70,6 +70,10 @@ def test_float_modify_and_query(self):
         self.config['types'] = {'myfloat': u'float'}
         item = self.add_item(artist=u'aaa')
 
+        # Do not match unset values
+        out = self.list(u'myfloat:10..0')
+        self.assertEqual(u'', out)
+
         self.modify(u'myfloat=-9.1')
         item.load()
         self.assertEqual(item['myfloat'], -9.1)
@@ -84,6 +88,10 @@ def test_bool_modify_and_query(self):
         false = self.add_item(artist=u'false')
         self.add_item(artist=u'unset')
 
+        # Do not match unset values
+        out = self.list(u'mybool:true, mybool:false')
+        self.assertEqual(u'', out)
+
         # Set true
         self.modify(u'mybool=1', u'artist:true')
         true.load()
@@ -112,6 +120,10 @@ def test_date_modify_and_query(self):
         old = self.add_item(artist=u'prince')
         new = self.add_item(artist=u'britney')
 
+        # Do not match unset values
+        out = self.list(u'mydate:..2000')
+        self.assertEqual(u'', out)
+
         self.modify(u'mydate=1999-01-01', u'artist:prince')
         old.load()
         self.assertEqual(old['mydate'], mktime(1999, 1, 1))