Import tags from text file

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)
4 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.

3 Likes

Hello, I’m looking for more hands on help with getting this plugin setup properly. Are you available to consult? My goal is to batch upload metadata to original tracks from a CSV file. I some what follow the thread, but not well enough to feel confident and so far not having luck.

Thanks for any and all help!

Hi, sorry but I went away for a while. Did you get that working ?

1 Like

LOL. Ummm…
A few hours ago, like before this was bumped. I was looking for the exact same thing, started to read the thread - saw the usual discussion and got distracted, never got to the end.

Did some other Google-Fu and came up with MP3Tag says it will do it with the Windows version, the Mac version hasn’t got nearly as much functionality yet.

Yate says it can do it. Makes a big deal about it in the features list. Try and actually do it with the application. Try and find anything about it in the actual documentation/online help/etc… What does Yate do that Picard can’t? The interface is for hell. But it purportedly can import tags from a CSV, and doesn’t say anything about separate files being required. Sidecar is mentioned, but not implied that it’s the only way. That’s okay, I can get Yate to export a CSV, but try to give it back? Pffft. May was well talk back to the TV.

This does the job, though you’re import data is a sidecar format format (single-file-per-track). If you had stuff in a database, you could make a report to spit that out with a properly formatted file name and let this chew on them.

@outsidecontext … by “Move the file.update() outside the loop” I gather you mean to place that line on par with the if/else, after the else?

Now, if you want to start building your own CSV file, on the Macintosh side of things, did you know that you can drop a directory/folder onto a BBEdit text document window … and all the contents in that directory are now listed line by line?

Nuke any common lines you don’t need, nuke the tabs, and then you can paste that right into Excel and use the filename columns to match up against your data to be imported. Conjure up a script that can export those a single file for each track name with the tags to be added in that file.

Or, import that into a FileMaker or similar database, make a new record for each line and add your tag data in an associated multi-line text field, one per line:

TAGNAME=TAGVALUE
TAGNAME=TAGVALUE

Set up a report/export function that would save the that text field into a file, getting the name from the other field, with an extension of .tag

So you’d have a file named

01. All Aboard![0∶30][256 44100KHz CBR 2ch].tag

Whose content is:

composer=Uncle Albert
mixedby=Admiral Halsey
date=2020-12-13

…and you’ll have those tags imported to that track.

In the script as it is shown above in @nadl40’s post it is already as it should be, it was updated after my comment :slight_smile:

3 Likes

Oh, yes. I missed that pencil +1 up there …
S’all good. Carry on. :slight_smile:

/me moves the line back to where it was.

1 Like

This functionality was meant for scripting tag files from whatever tag source there is.

Yeah, that makes sense, and if the tag source is yourself creating them in a database, then … that should work. :slight_smile:

Am I correct on that the filename is .tag and the only thing in the file would be a single line for each tag name = tag value?

Yes, you can have multiple tags and multiple occurrences per tag, for example ARTIST.
Here is an example:

COMPOSER=Beethoven, Ludwig van
TITLE=Triple Concerto, op. 56: I. Allegro
WORK=Op. 58: Piano Concerto No. 4 in G major
MOVEMENTNAME=I. Allegro
ARTIST=Truls Mørk (vc)
ARTIST=Gil Shaham (vn)
ARTIST=Yefim Bronfman (pf)
ARTIST=Tonhalle Orchester Zürich (orch)
ARTIST=David Zinman (con)
ALBUM=Beethoven: Triple Concerto, Septet (Rec 2004,Rel 2006)
ALBUMARTIST=David Zinman, Yefim Bronfman, Gil Shaham, Truls Mørk

1 Like

Which means it supports the stacking of tags properly. Good deal.

Heh, has me wondering now, what if the reverse could be made to export the .tag files, so they could be edited and brought back in. :slight_smile: