lv2 / lilv

LV2 host library
ISC License
37 stars 20 forks source link

Add example Python script to list a plugin's presets #27

Closed SpotlightKid closed 5 years ago

SpotlightKid commented 5 years ago

This was something that was a bit hard to figure out how to do, so maybe it is worth adding it as an example.

SpotlightKid commented 5 years ago

The script also demonstrates a few points where the Python binding feels arkward:

  1. To get a new URI from a Namespace instance by adding a suffix, which isn't a valid Python identifier, you can't use attribute nottaion (ns.suffix), but have to use getattr( ns, '#suffix'). IMO Namespace instances should support the + operator by adding a __add__ magic method to the class.
  2. If you pass an invalid URI string to World.new_uri(), it prints error: attempt to map invalid URI XXX' and returns None. IMO it should raise an exception.
  3. If you look up a plugin in a Plugins instance via dictionary key access (e.g. plugins[plugin_uri], if the plugin_uri is not contained in the collection, the lookup returns None. IMO it should raise a KeyError instead.
drobilla commented 5 years ago

You are having this problem because you left the trailing separator out of the namespace prefix for some reason.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""List all presets of an LV2 plugin with the given URI."""

import sys
import lilv

PRESET_NS = 'http://lv2plug.in/ns/ext/presets#'
RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#'

def main(args=None):
    args = sys.argv[1:] if args is None else args

    if args:
        uri = args[0]
    else:
        return "Usage: lv2-list-plugin-presets <plugin URI>"

    world = lilv.World()
    world.load_all()
    preset_ns = lilv.Namespace(world, PRESET_NS)
    rdfs_ns = lilv.Namespace(world, RDFS_NS)
    plugins = world.get_all_plugins()
    plugin_uri = world.new_uri(uri)

    if plugin_uri is None or plugin_uri not in plugins:
        return "Plugin with URI '%s' not found" % uri

    plugin = plugins[plugin_uri]
    presets = plugin.get_related(preset_ns.Preset)

    preset_list = []

    for preset in presets:
        labels = world.find_nodes(preset, rdfs_ns.label, None)

        if labels:
            label = str(labels[0])
        else:
            label = str(preset)
            print("Preset '%s' has no rdfs:label" % preset, file=sys.stderr)

        preset_list.append((label, str(preset)))

    for preset in sorted(preset_list):
        print("%s: %s" % preset)

if __name__ == '__main__':
    sys.exit(main() or 0)
SpotlightKid commented 5 years ago

True, in this case it can be solved that way, but attribute notation is not a general solution IMO. What if there is any other non-identifier char in the ~pre~suffix, e.g. a question mark or a slash?

Thatswhy I submitted PR #28.

drobilla commented 5 years ago

Well, yes, technically this makes the mechanism less powerful, but the convention is the same as in the Turtle files: such things just can't be in the name at all. I prefer it this way because they line up (e.g. the original way you did it was weird, the API being this way more or less forces you to define the prefix properly). It would only cause more confusion down the road to have namespaces defined inconsistently in various places.

drobilla commented 5 years ago

Also, good point about KeyError. There are probably lots of things in there that aren't properly Pythonic. Patches welcome.

drobilla commented 5 years ago

7a7bff2

SpotlightKid commented 5 years ago

Updated the script to work with the new exception raising behavior.

Feel free to squash the commits when merging.

SpotlightKid commented 5 years ago

Did a few more changes and then rebased on master and squashed the commits:

drobilla commented 5 years ago

Incorporated in slightly simplified form as a4b3ca8, thanks.