Trouble replicating aunique() functionality


#1

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.


#2

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.


#3

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.


#4

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:


#5

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

#6

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!