Trouble replicating aunique() functionality

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. :slight_smile:

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 libs 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 items 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!