Import tags from text file

Tags: #<Tag:0x00007f820bd59500>

Is there a way in Picard to import track (or album) metadata from a simple text file ?
A simple csv like this will do

TAGNAME,delimiter,TAGVALUE

or preferably vorbis comments style

TAGNAME=TAGVALUE

Album level would require a structure, so perhaps just at the track level is easier.
I did not see a plugin that can do that.

If not, would anybody know of sample python code that can I can turn into a personal plugin ? I’m not very familiar with python, can only do a simple code.

That is currently not possible, but a plugin could do it.

A plugin would need to implement a file_post_load_processor, which gets called when a file gets loaded.

There is one existing plugin that could be used as a starting point: haikuattrs loads additional tag data from file system metadata on the Haiku OS. You have to ignore the noise here, but the gist is this:

from picard.file import register_file_post_load_processor


def on_file_load_processor(file):
    # Read the data from file and add tags to file.metadata with e.g.
    # file.metadata["title"] = the_title
    file.update()  # Ensures the tag changes are immediately shown in the UI


register_file_post_load_processor(on_file_load_processor)
5 Likes

Thanks, I will give it a try and report back, perhaps others can find it usefull.

1 Like

@nadl40 You are not the only one trying to do this:

1 Like

Almost there but the update is not happening despite what the logs is indicating. I tried custom and regular tags.

Let me paste the code here (is very basic and you can tell I’m not a python developer :slight_smile: )

# load tags from file
PLUGIN_NAME = u'AAA Add tags from text file to a track'
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ["2.0"]

from picard.file import register_file_post_load_processor
from picard import log
import csv
import os.path

def on_file_load_processor(file):

   # get track file name to find corresponding .tag file
   fileAndPath = os.path.splitext(file.filename)[0]
   tagFile = fileAndPath + ".tag" 

   if os.path.isfile(tagFile):
      # read tag file
      with open(tagFile) as csvfile:
         readCSV = csv.reader(csvfile, delimiter='=')
         tags = []
         values = []
         for row in readCSV:
            tag = row[0]
            value = row[1]
            position = tags.count(tag) 
            if position > 0 :
                  values[position] = values[position] + ';' + value    
            else:
                  tags.append(tag)
                  values.append(value)

      # add to Picard     
      for index, tag in enumerate(tags): 
         value = values[index]
         log.debug('\ttag \t%s \tvalue \t%s' % (tag, value))
         file.metadata[tag] = value
      file.update()  

   else:
      log.debug('tag file \t%s does not exists' % tagFile)
      
register_file_post_load_processor(on_file_load_processor)

Any pointers ?

Works for me, but I have a suspicion: This currently only works for loaded files. But as soon as the files get automatically matched to releases (because you have already tagged them) you won’t see the changes, because the track does not have it.

There are two ways to fix this, depending on what you want this plugin to do.

Option 1: The .tag files contains the original tags, but these should be overridden by MusicBrainz metadata. Still you want to see the changes. For this you need to modify file.orig_metadata also. Update on_file_load_processor to also do:

for index, tag in enumerate(tags): 
    value = values[index]
    log.debug('\ttag \t%s \tvalue \t%s' % (tag, value))
    file.metadata[tag] = value
    file.orig_metadata[tag] = value
file.update()  

If you tag the file now it will show the tags as changed if the MB data differs.

Option 2: You want to always apply the tags from the .tag file, overwriting any data from MusicBrainz.

For this this we also need to run it in register_file_post_addition_to_track_processor.

I tweaked the plugin a bit. It only runs reading the actual .tag file when loading the file and stores the results in file._external_tags:

# load tags from file
PLUGIN_NAME = u'AAA Add tags from text file to a track'
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ["2.0"]

from collections import defaultdict
import csv
import os.path

from picard import log
from picard.file import (
   register_file_post_addition_to_track_processor,
   register_file_post_load_processor,
)


def read_tags_from_file(file):

   # get track file name to find corresponding .tag file
   fileAndPath = os.path.splitext(file.filename)[0]
   tagFile = fileAndPath + ".tag"
   tags = defaultdict(list)

   if os.path.isfile(tagFile):
      # read tag file
      with open(tagFile) as csvfile:
         readCSV = csv.reader(csvfile, delimiter='=')
         for row in readCSV:
            tag = row[0]
            value = row[1]
            tags[tag].append(value)

   else:
      log.debug('tag file \t%s does not exists' % tagFile)

   return tags


def on_file_add_to_track_processor(track, file):
   if hasattr(file, '_external_tags'):
      for tag, values in file._external_tags.items():
         log.debug('\ttag \t%s \tvalue \t%s' % (tag, values))
         file.metadata[tag] = values
         track.metadata[tag] = values
      file.update()
      track.update()
   else:
      log.debug('no external tags loaded for file %s' % file.filename)


def on_file_load_processor(file):
   file._external_tags = read_tags_from_file(file)
   for tag, values in file._external_tags.items():
      log.debug('\ttag \t%s \tvalue \t%s' % (tag, values))
      file.orig_metadata[tag] = values
      file.metadata[tag] = values
   file.update()


register_file_post_addition_to_track_processor(on_file_add_to_track_processor)
register_file_post_load_processor(on_file_load_processor)
2 Likes

I’m after the first option, I will be using custom tags so I can further tag script them to apply to regular tags in Picard. This is exactly what I was after.

There might be some interest in this private plugin. It is limited because it requires a .tag file for each track, in my case I will script them so that will work. Creating .tag file on an album level would require a structure. Perhaps a real python developer can turn this simple plugin into a community plugin.

Here is my final code for what is worth (thanks @outsidecontext for the smarter list processing)

Tags format is in vorbis comment style

TAGNAME=TAGVALUE

plugin

PLUGIN_NAME = u'AAA Add tags from text file to a track'
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ["2.0"]

from picard.file import register_file_post_load_processor
from picard import log
import csv
import os.path
from collections import defaultdict

def on_file_load_processor(file):

   # get track file name to find corresponding .tag file
   fileAndPath = os.path.splitext(file.filename)[0]
   tagFile = fileAndPath + ".tag" 
   tags = defaultdict(list)

   if os.path.isfile(tagFile):

      # read tag file
      with open(tagFile) as csvfile:
         readCSV = csv.reader(csvfile, delimiter='=')
         for row in readCSV:
            tag = row[0]
            value = row[1]
            tags[tag].append(value)

      # add to Picard     
      for tag, values in tags.items():
         log.debug('\ttag \t%s' % tag)
         log.debug('\tvalue \t%s' % values)
         file.metadata[tag] = values
         file.orig_metadata[tag] = values
      file.update()  

   else:
      log.debug('tag file \t%s does not exists' % tagFile)
      
register_file_post_load_processor(on_file_load_processor)
3 Likes

Great. One small fix: Move the file.update() outside the loop. file.update() is kind of expensive, as it triggers updating the UI for any changes made. Best to run this only once per file after all tags have been changed.

2 Likes