beetbox / beets

music library manager and MusicBrainz tagger
http://beets.io/
MIT License
12.85k stars 1.82k forks source link

In 2.0.0 (from git snapshot), running 'beet splupdate' (for smartplaylist) fails with TypeError whenever 'playlist:x' is in any query #5354

Open charliec111 opened 3 months ago

charliec111 commented 3 months ago

Problem

This arose after updating to 2.0.0 (I had previously been using 1.6.1 from pip, this did not appear. It looks like 2.0 has been out, I must not have updated in a while.). I've confirmed this on Debian 12.6 Stable running python 3.11 and another machine running Fedora and python 3.12. From commenting out all smart playlists and adding them back until the error reappeared, I've found whenever any smartplaylist query includes "playlist:x", "beet splupdate" fails with the python error "TypeError: unhashable type: 'list'". (I'm using a minimal config and a test playlist that I've confirmed produce the same issue instead of my usual ones. The full config is >400 lines, mostly irrelevant. The verbose mode of playlist plugin will print out the full path to every item in every playlist, which led to a line of over 40,000 chars in the error output.) 'playlist' and 'smartplaylist' are the only enabled plugins. testplaylist.m3u is a playlist. smartplaylist is configured with 1 query (query: playlist:"testplaylist"). Running this command in verbose (-vv) mode:

$ beet -c config.test.yaml -vv splupdate

Led to this problem:

overlaying configuration: config.test.yaml
user configuration: /home/username/.config/beets/config.yaml
data directory: /home/username/.config/beets
plugin paths: 
Sending event: pluginload
library database: /home/username/.config/beets/library.db
library directory: /home/username/Music
Sending event: library_opened
Parsed query: AndQuery([PlaylistQuery('path', [b"/home/username/Music/Charli XCX/how i'm feeling now/08 c2.0.m4a", b'/home/username/Music/Louis Armstrong/The Ultimate Collection/02-41 You Rascal, You.m4a', b'/home/username/Music/Lizzo/Special/01-01 The Sign.m4a', b'/home/username/Music/Tom Petty and the Heartbreakers/Greatest Hits/10 You Got Lucky.m4a', b'/home/username/Music/Daryl Hall & John Oates/The Essential Daryl Hall & John Oates/01-17 Private Eyes.m4a'], fast=True)])
Parsed sort: NullSort()
Traceback (most recent call last):
  File "/home/username/.local/bin//beet", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/username/.local/lib/python3.11/site-packages/beets/ui/__init__.py", line 1865, in main
    _raw_main(args)
  File "/home/username/.local/lib/python3.11/site-packages/beets/ui/__init__.py", line 1852, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/home/username/.local/lib/python3.11/site-packages/beetsplug/smartplaylist.py", line 130, in update_cmd
    self.build_queries()
  File "/home/username/.local/lib/python3.11/site-packages/beetsplug/smartplaylist.py", line 224, in build_queries
    self._unmatched_playlists.add(playlist_data)
  File "/home/username/.local/lib/python3.11/site-packages/beets/dbcore/query.py", line 514, in __hash__
    return reduce(mul, map(hash, self.subqueries), 1)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/username/.local/lib/python3.11/site-packages/beets/dbcore/query.py", line 185, in __hash__
    return hash((self.field_name, hash(self.pattern)))
                                  ^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'

Here's a link to the music files that trigger the bug (if relevant):

Setup

My configuration (output of beet config) is (the minimal config that produces the issue above):

directory: /home/username/Music/
format_item: %ifdef{favorite,%if{$favorite > 0,❤ ,},}%if{$albumartist,$albumartist,$artist} - %if{$album,$album,None} - $title %if{%ifdef{play_count},($play_count),(-)}
format_album: $albumartist - $album ($album_play_count)
per_disc_numbering: yes
original_date: yes
sort_album: albumartist+ year+ month+ day+ album+
sort_item: favorite- live+ title+ albumartist+ year+ album+ disc+ track+
plugins: playlist smartplaylist
playlist:
  relative_to: /home/username/Music/Playlists/
  playlist_dir: /home/username/Music/Playlists/
smartplaylist:
    auto: no
    relative_to: /home/username/Music/
    playlist_dir: /home/username/Music/Playlists/
    forward_slash: no
    prefix: '../'
    playlists:
      - name: "test playlist beets smartplaylist.m3u"
        query: 'playlist:"testplaylist"'

I haven't tested this much but I have a potential solution. Based on the TypeError pointing to self.pattern on line 185 in 'beets/dbcore/query.py' it seemed like changing self.pattern into another type would fix it. In 'beets/dbcore/query.py', on line 184 in class FieldQuery, I changed:

    def __hash__(self) -> int:
        return hash((self.field_name, hash(self.pattern)))

to:

    def __hash__(self) -> int:
        try:
            return hash((self.field_name, hash(self.pattern)))
        except TypeError:
            return hash((self.field_name, hash(tuple(self.pattern))))

After this, there is no error and it produces the playlists (but I haven't really looked at all at query.py to know if this could cause problems with something else).

wisp3rwind commented 3 months ago

For whoever looks into this in more depth: The issue is with InQuery (subclassed by PlaylistQuery), which has pattern type Sequence[AnySQLiteType], which is not guaranteed to be hashable depending on the sequence type. Maybe the best solution would be to override __hash__ in InQuery? It might also be worth exploring if we can use the type checker to verify that concreate pattern types support the hashable protocol?

While at it, InQuery should probably also override __eq__ in a way that ignores ordering of the pattern type.

Moreover, I don't think the smartplaylist plugin should be hashing the queries in the first place: This happens because they end up being put in a set. However, at a first glance, the playlist name is a unique key, so rather than sets, dicts should be used.

Related: https://github.com/beetbox/beets/pull/5210/