Get files from objects

I’m just putting some finishing touches to the Classical Extras plugin, but have a bit of a head-scratch:

  1. It’s possible to get a list of file names in an album by using
    album.tagger.get_files_from_objects([album])
    but if you try and do the same for just a track, it returns an empty list, even though the get_files_from_objects function says
    Return list of files from list of albums, clusters, tracks or files.
  2. Also, the list of files is just the filenames, not the full path. I can get a list of files with the full path by using album.tagger.files but that returns a list of every file loaded in Picard and I can’t see a way to restrict it to just the files linked with a specific object (album or track).
  3. I could, of course, get a list of paths and filenames for an album by matching the two lists, but that still doesn’t solve the problem of getting the full path and filename for a specific track object. At the moment, my work-round is to look for matching metadata, but that only works properly if the existing file has MBIDs.*

Any suggestions?

*Edit: Actually, that’s a bit misleading as, assuming the file exists, you can match on the track id that Picard has just assigned from the lookup - i.e. match track.metadata['musicbrainz_trackid'] to album.tagger.files[filename].metadata['musicbrainz_trackid']
These should both exist. However for some reason (possibly network issues) Picard doesn’t always get the latter.

Does this help?

1 Like

In the thread at Plugin development: get file from track, the suggestion is to use register_file_action(xxx). My understanding is that this function is in itemsview.py - i.e. for interaction with items in the main ui. A good example of this use is the viewvariables plugin. To get the files, viewvariables uses the get_files_from_objects(objs) method referred to in my OP. So I’m not sure that helps a lot in the current case.

I’ve done a bit more research and observed the following behaviour in Picard when using the register_track_metadata_processor() API:

  1. If a new album (i.e. not yet tagged by Picard and with no MBIDs) is loaded, clustered and looked-up/scanned, resulting in the matched files being shown in the right-hand pane, then:
  • track.metadata gives the lookup result from MB - i.e. no file information, just the track info.
  • album.tagger.files[xxxx].metadata (where xxxx is the path/filename of the track) gives the file information (dirname etc.) and the tags on the original file.
  • album.tagger.files[xxxx].orig_metadata gives the same as album.tagger.files[xxxx].metadata
  1. However, if the album is then “refreshed”, this does not just carry out a repeat operation, instead:
  • track.metadata gives the same as before
  • album.tagger.files[xxxx].metadata gives the metadata as in track.metadata and also includes all the file information as before.
  • album.tagger.files[xxxx].orig_metadata gives the same as before (i.e. the original tags).

So, the ‘post-refreshment’ metadata is actually usable - by matching the musicbrainz_trackid of track.metadata and album.tagger.files[xxxx].metadata, you can get the file details of the track. Not elegant, but it works.
But why is it necessary to “refresh” to get what seems like a logical data set? And I still don’t know why get_files_from_objects(objs) doesn’t work in the register_track_metadata_processor() API when objs is a list of tracks, but does provide a list of file names (without paths) when objs is a list of albums. Also it does work in the register_file_action(xxx)/register_track_action(xxx) API, but I assume that is because itemsview.py can identify that you have clicked on an item that is a track and a file.

If any devs are reading this, I would be grateful for enlightenment (and for forgiveness if I have expressed this somewhat clumsily!)

1 Like

I don’t think you can do what you want as things are not finalised yet.
I think the order is something like the below:

  1. Files are loaded in to picard.
  2. Picard retrieves metadata from the api.
  3. Plugins are called
  4. scripts are called
    metadata has finished loading and all the metadata is ready to be applied.
2 Likes

Thanks. So basically, the metadata process is not complete when the plugin is called, so not all metadata is available to it. In which case, for my plugin, I will have to live with the current approach, which is to give the user a message that existing file metadata has not been loaded and they will need to “refresh” if they want it - not elegant but it sort of works. I suspect that 2.0 will not be more ‘plugin-friendly’ than 1.4 (I haven’t attempted the migration yet), but one can hope…

It’s like dns_server said. The album and track metadata processors are executed after data has been loaded, but before files are matched. Even if it would match the files before it it would be error prone to use it that way. There is no guarantee that the files have all been matched correctly, and the metadata processor is not executed again when the matching is updated.

What you probably would need is a metadata processor that gets called whenever a file gets matched to a track. That way it would always be useful, no matter how or when a file gets matched to a track. It does not currently exist, but I see how this could be useful and could be added.

For now instead of relying on a refresh, why not add an explicit context menu action? See https://picard.musicbrainz.org/docs/plugin-api/

Depending for what exactly you need it for you can use register_album_action, register_track_action or register_file_action

I would like a plugin hook to be able to do this sort of thing.
For my plugin I would like to download images for artist and save a copy of the wikipedia page and to do that I would like to know where to save these files.
If we could have a call back when the metadata has finished loading, all the scripts have run and when the file has been saved we would be able to do a few more things.

Just in case it is of use to anyone else. I have found a bit of a hack that enables a plugin using the register_track_metadata_processor() API to get the filenames for a track. This is illustrated in the following code snippet:

album_filenames = album.tagger.get_files_from_objects([album])
trackno = track.metadata['tracknumber']
discno = track.metadata['discnumber']
track_file = None
for album_file in album_filenames:
    if str(album_file.tracknumber) == trackno and str(album_file.discnumber) == discno:
        track_file = album_file.filename
        break

This, of course, assumes that Picard has managed to match the files correctly.
EDIT:
BTW to use the above code, you need to get the track object first, for which you can use the hack track = album._new_tracks[-1]. Otherwise, replace track.metadata by the metadata argument in the function which is the argument in register_track_metadata_processor()

3 Likes

Having tested the above code in the development version of Classical Extras, the only snag so far is that get_files_from_objects(objs) doesn’t always return all the files for the album object. Mostly it does, but randomly and inconsistently it doesn’t. I think it may be a network-related issue as I haven’t seen it happen on the local drive. I recall another post about network issues and file saving - Save 'errors' - and there have been various other network issues with Picard, but whether this is related I do not know.

I think that’s just not something you can reliable do right now. The current plugin hooks don’t make any guarantee that files have been matched, also no matter what you do it will only work in special cases. As files can be matched by various methods at any time, the existing hooks that get called when metadata gets loaded just won’t be reliable.

What you’d need is a API hook that gets called whenever a file gets matched to a track, and I think such a hook would be a good idea to have in Picard.

3 Likes