I’m trying to replicate the functionality of aunique() in my own plugin. Eventual goal is a function called solo() that returns True or False depending on how many albums match the disambiguators given. I copy and paste the entire aunique() function from library.py, but that’s not enough. My copy of aunique returns this: ˂’str’ object has no attribute 'item’˃. Looking at the source, I see where ‘.item’ attributes are used. I assume that aunique gains functionality from the rest of library.py, but I don’t understand what I should do (what code should be copied) to add that functionality to my plugin.
Hmmm—it’s pretty hard to say without seeing the code you’re using currently. Is this for an inline
definition, or a new custom plugin? The latter, FWIW, might be the easiest to get to match %aunique{}
exactly, because an inline
definition has a different structure than an ordinary Python function.
Yeah, I opted for a plugin because it looked more robust for advanced python coding. I use this album: http://freemusicarchive.org/music/Broke_For_Free/Directionless_EP/
Here is solo.py. After the first few lines it is 100% a copy-paste of aunique from here: https://github.com/beetbox/beets/blob/master/beets/library.py#L1487
from beets.plugins import BeetsPlugin
class SoloPlugin(BeetsPlugin):
def __init__(self):
super(SoloPlugin, self).__init__()
self.template_funcs['solo'] = tmpl_solo
def tmpl_solo(self, keys=None, disam=None, bracket=None):
"""Generate a string that is guaranteed to be unique among all
albums in the library who share the same set of keys. A fields
from "disam" is used in the string if one is sufficient to
disambiguate the albums. Otherwise, a fallback opaque value is
used. Both "keys" and "disam" should be given as
whitespace-separated lists of field names, while "bracket" is a
pair of characters to be used as brackets surrounding the
disambiguator or empty to have no brackets.
"""
# Fast paths: no album, no item or library, or memoized value.
if not self.item or not self.lib:
return u''
if self.item.album_id is None:
return u''
memokey = ('aunique', keys, disam, self.item.album_id)
memoval = self.lib._memotable.get(memokey)
if memoval is not None:
return memoval
keys = keys or 'albumartist album'
disam = disam or 'albumtype year label catalognum albumdisambig'
if bracket is None:
bracket = '[]'
keys = keys.split()
disam = disam.split()
# Assign a left and right bracket or leave blank if argument is empty.
if len(bracket) == 2:
bracket_l = bracket[0]
bracket_r = bracket[1]
else:
bracket_l = u''
bracket_r = u''
album = self.lib.get_album(self.item)
if not album:
# Do nothing for singletons.
self.lib._memotable[memokey] = u''
return u''
# Find matching albums to disambiguate with.
subqueries = []
for key in keys:
value = album.get(key, '')
subqueries.append(dbcore.MatchQuery(key, value))
albums = self.lib.albums(dbcore.AndQuery(subqueries))
# If there's only one album to matching these details, then do
# nothing.
if len(albums) == 1:
self.lib._memotable[memokey] = u''
return u''
# Find the first disambiguator that distinguishes the albums.
for disambiguator in disam:
# Get the value for each album for the current field.
disam_values = set([a.get(disambiguator, '') for a in albums])
# If the set of unique values is equal to the number of
# albums in the disambiguation set, we're done -- this is
# sufficient disambiguation.
if len(disam_values) == len(albums):
break
else:
# No disambiguator distinguished all fields.
res = u' {1}{0}{2}'.format(album.id, bracket_l, bracket_r)
self.lib._memotable[memokey] = res
return res
# Flatten disambiguation value into a string.
disam_value = album.formatted(True).get(disambiguator)
# Return empty string if disambiguator is empty.
if disam_value:
res = u' {1}{0}{2}'.format(disam_value, bracket_l, bracket_r)
else:
res = u''
self.lib._memotable[memokey] = res
return res
Here is my config.yaml:
directory: e:\test\music\
library: e:\test\music\beetslibrary.bib
import:
copy: yes
duplicate_action: keep
plugins: solo
pluginpath: C:\apps\dial-beets\beetsplug
paths:
default: a%aunique{albumartist,year,}b%solo{albumartist,year,}c/$title
I’m aiming to replicate aunique exactly, so I can debug my implementation of the functionality that is based on aunique.
After importing the album a few times, the folders look like this:
a 7b_'str' object has no attribute 'item'_c
The 7
with a leading space comes from aunique, working perfectly. My copy of aunique, in the “solo” function, is returning: _'str' object has no attribute 'item'_
. Just to test, when I make this solo.py
, it works as expected.
from beets.plugins import BeetsPlugin
class SoloPlugin(BeetsPlugin):
def __init__(self):
super(SoloPlugin, self).__init__()
self.template_funcs['solo'] = tmpl_solo
def tmpl_solo(self, keys=None, disam=None, bracket=None):
return 'my_test_str2'
It returns a 8bmy_test_str2c
.
The most immediate problem here is that your function has a self
parameter, but it’s not a method. In Python, only methods get passed self
automatically. And the reference to self.item
won’t work without a reference to the item being templated.
It might be helpful to try using print-debugging inside the code to see what parameters are being passed, etc. It’s a time-honored tradition: just insert a few print() calls here and there to check whether the values are what you expect them to be, and you may be surprised.
Thank you. I’ve spent some time with this and still don’t have it working properly. There are only four path-format plugins within beets, so I don’t have much to work from.
I could probably use some help understanding this:
And the reference to
self.item
won’t work without a reference to the item being templated.
I can’t get my plugin to inherit Library lib
and/or Item item
attributes, which I assume are higher up in the hierarchy somewhere. aunique is of this class:
class DefaultTemplateFunctions(object):
and object
is a class in Python. Since it’s built-in to Python, I assume lib
and item
don’t come from there. I notice that aunique is the only item within the DefaultTemplateFunctions
class to not be a staticmethod. So maybe the magic happens from the DefaultTemplateFunctions
class? In the init
we have this, and a comment specifically referencing aunique
.
self.item = item
self.lib = lib
But that’s as close as I can find to where item
or lib
s come from. It looks like when plugins add commands to the beet command line, they can just call functions like this: myFunction(lib, item)
and have them work as expected after some setup with register_listener
. Take importadded.py
:
register = self.register_listener
#snip
register('item_imported', self.update_item_times)
#snip
def update_item_times(self, lib, item):
These would presumably get lib
and item
s from the BeetsPlugin
class? This is my OS:
- Windows 10 x64
- Python 3.6.2
- beets 1.4.5
Hello! Have you considered starting with the second example in this section of the docs?
http://docs.beets.io/en/v1.4.5/dev/plugins.html#add-path-format-functions-and-fields
That one, which adds a disc_and_track
field, shows how the callback function registered with template_fields
gets an item
as a parameter. Would that be enough to pull out the information your code needs!